Java Persistence – механизм, помогающий обеспечить сохранность данных после завершения программы, что является гла
131 90 4MB
Russian Pages 632 Year 2017
Кристиан Бауэр, Гэвин Кинг, Гэри Грегори
Java Persistence API и Hibernate
Christian Bauer, Gavin King, Gary Gregory
Java Persistence with Hibernate Second Edition
MANNING SHELTER ISLAND
Кристиан Бауэр, Гэвин Кинг, Гэри Грегори
Java Persistence API и Hibernate
Москва, 2017
УДК 04.655.3Hibernate ББК 32.973.2 Б29 Бауэр К., Кинг Г., Грегори Г. Б29 Java Persistence API и Hibernate / пер. с англ. Д. А. Зинкевича; под науч. ред. А. Н. Киселева. – М.: ДМК Пресс, 2017. – 632 с.: ил. ISBN 978-5-97060-180-8 Java Persistence – механизм, помогающий обеспечить сохранность данных после завершения программы, что является главной чертой современных приложений. Hibernate – наиболее популярный инструмент Java для работы с базами данных, предоставляющим автоматическое прозрачное объектно-реляционное отображение, что значительно упрощает работу с SQL-базами данных в приложениях Java. Данная книга описывает разработку приложения с использованием Hibernate, связывая воедино сотни отдельных примеров. Также вы найдете хорошо иллюстрированное обсуждение лучших методик проектирования баз данных и методов оптимизации. Издание предназначено разработчикам, знакомым с языком Java.
УДК 04.655.3Hibernate ББК 32.973.2
Original English language edition published by Manning Publications USA, USA. Copyright (c) 2015 by Manning Publications. Russian‐language edition copyright (c) 2015 by DMK Press. All rights reserved. Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. Материал, изложенный в данной книге, многократно проверен. Но поскольку вероятность технических ошибок все равно существует, издательство не может гарантировать абсолютную точность и правильность приводимых сведений. В связи с этим издательство не несет ответственности за возможные ошибки, связанные с использованием книги.
ISBN 978-1-617-29045-9 (анг.) ISBN 978-5-97060-180-8 (рус.)
© 2016 by Manning Publications Co. © Оформление, издание, перевод, ДМК Пресс, 2017
Александру, за то, что научил меня, как учить его. – Гэри Грегори
Содержание Предисловие к первому изданию............................................................................. 16 Введение........................................................................................................................................... 18 Благодарности............................................................................................................................. 19 Об этой книге................................................................................................................................. 21 Об изображении на обложке.......................................................................................... 24 Часть I. Начинаем работать с ORM........................................................................... 25 Глава 1. Основы объектно-реляционного отображения..................... 26 1.1. Что такое долговременное хранение?.................................................................................... 27 1.1.1. Реляционные базы данных................................................................................................ 28 1.1.2. Разбираемся с SQL............................................................................................................... 29 1.1.3. Использование SQL в Java................................................................................................ 30 1.2. Несоответствие парадигм........................................................................................................... 32 1.2.1. Проблема детализации....................................................................................................... 33 1.2.2. Проблема подтипов.............................................................................................................. 35 1.2.3. Проблема идентичности..................................................................................................... 37 1.2.4. Проблемы, связанные с ассоциациями......................................................................... 38 1.2.5. Проблемы навигации по данным.................................................................................... 39 1.3. ORM и JPA....................................................................................................................................... 41 1.4. Резюме............................................................................................................................................... 43
Глава 2. Создаем проект.................................................................................................... 44 2.1. Представляем Hibernate............................................................................................................. 44 2.2. «HELLO WORLD» и JPA........................................................................................................... 45 2.2.1. Настройка единицы хранения.......................................................................................... 46 2.2.2. Хранимый класс.................................................................................................................... 47 2.2.3. Сохранение и загрузка сообщений................................................................................. 49 2.3. Оригинальная конфигурация Hibernate............................................................................... 51 2.4. Резюме............................................................................................................................................... 54
Глава 3. Модели предметной области и метаданные. ......................... 55 3.1. Учебное приложение CaveatEmptor....................................................................................... 56 3.1.1. Многоуровневая архитектура.......................................................................................... 56 3.1.2. Анализ предметной области............................................................................................. 58 3.1.3. Предметная модель приложения CaveatEmptor....................................................... 59
Содержание 7 3.2. Реализация предметной модели.............................................................................................. 61 3.2.1. Предотвращение утечек функциональности.............................................................. 61 3.2.2. Прозрачность сохранения и его автоматизация........................................................ 62 3.2.3. Создание классов с возможностью сохранения......................................................... 64 3.2.4. Реализация ассоциаций в POJO.................................................................................... 67 3.3. Метаданные предметной модели............................................................................................. 72 3.3.1. Определение метаданных с помощью аннотаций..................................................... 73 3.3.2. Применение правил валидации компонентов............................................................ 75 3.3.3. Метаданные во внешних XML-файлах......................................................................... 78 3.3.4. Доступ к метаданным во время выполнения.............................................................. 82 3.4. Резюме............................................................................................................................................... 86
Часть II. Стратегии отображения............................................................................... 87 Глава 4. Отображение хранимых классов. ....................................................... 88 4.1. Понятие сущностей и типов-значений.................................................................................. 88 4.1.1. Хорошо детализированные модели предметной области....................................... 89 4.1.2. Определение сущностей приложения........................................................................... 89 4.1.3. Разделение сущностей и типов-значений.................................................................... 91 4.2. Отображение сущностей с идентичностью.......................................................................... 93 4.2.1. Идентичность и равенство в Java.................................................................................... 93 4.2.2. Первый класс сущности и его отображение................................................................ 94 4.2.3. Выбор первичного ключа................................................................................................... 95 4.2.4. Настройка генераторов ключей....................................................................................... 97 4.2.5. Стратегии генерации идентификаторов....................................................................... 99 4.3. Способы отображений сущностей.........................................................................................103 4.3.1. Управление именами.........................................................................................................103 4.3.2. Динамическое формирование SQL..............................................................................106 4.3.3. Неизменяемые сущности.................................................................................................107 4.3.4. Отображение сущности в подзапрос............................................................................108 4.4. Резюме.............................................................................................................................................110
Глава 5. Отображение типов-значений.............................................................111 5.1. Отображение полей основных типов...................................................................................112 5.1.1. Переопределение настроек по умолчанию для свойств основных типов.......112 5.1.2. Настройка доступа к свойствам.....................................................................................114 5.1.3. Работа с вычисляемыми полями...................................................................................116 5.1.4. Преобразование значений столбцов............................................................................117 5.1.5. Значения свойств, генерируемые по умолчанию....................................................118 5.1.6. Свойства для представления времени.........................................................................119 5.1.7. Отображение перечислений...........................................................................................120 5.2. Отображение встраиваемых компонентов....................................................................121 5.2.1. Схема базы данных.............................................................................................................121
8 Содержание 5.2.2. Встраиваемые классы........................................................................................................122 5.2.3. Переопределение встроенных атрибутов...................................................................125 5.2.4. Отображение вложенных встраиваемых компонентов.........................................126 5.3. Отображение типов Java и SQL с применением конвертеров.....................................128 5.3.1. Встроенные типы................................................................................................................128 5.3.2. Создание собственных конвертеров JPA....................................................................135 5.3.3. Расширение Hibernate с помощью пользовательских типов..............................141 5.4. Резюме.............................................................................................................................................148
Глава 6. Отображение наследования..................................................................150 6.1. Одна таблица для каждого конкретного класса и неявный полиморфизм............151 6.2. Одна таблица для каждого конкретного класса с объединениями............................153 6.3. Единая таблица для целой иерархии классов...................................................................156 6.4. Одна таблица для каждого подкласса с использованием соединений.....................159 6.5. Смешение стратегий отображения наследования...........................................................163 6.6. Наследование и встраиваемые классы................................................................................165 6.7. Выбор стратегии..........................................................................................................................168 6.8. Полиморфные ассоциации.......................................................................................................170 6.8.1. Полиморфная ассоциация многие к одному (many-to-one)..................................170 6.8.2. Полиморфные коллекции................................................................................................173 6.9. Резюме.............................................................................................................................................174
Глава 7. Отображение коллекций и связей между сущностями. ..............................................................................................................175 7.1. Множества, контейнеры, списки и словари с типами-значениями............................176 7.1.1. Схема базы данных..............................................................................................................176 7.1.2. Создание и отображение поля коллекции..................................................................176 7.1.3. Выбор интерфейса коллекции........................................................................................178 7.1.4. Отображение множества...................................................................................................180 7.1.5. Отображение контейнера идентификаторов.............................................................181 7.1.6. Отображение списка...........................................................................................................182 7.1.7. Отображение словаря.........................................................................................................184 7.1.8. Отсортированные и упорядоченные коллекции.......................................................185 7.2. Коллекции компонентов...........................................................................................................188 7.2.1. Равенство экземпляров компонентов...........................................................................189 7.2.2. Множество компонентов...................................................................................................191 7.2.3. Контейнер компонентов....................................................................................................193 7.2.4. Словарь с компонентами в качестве значений.........................................................194 7.2.5. Компоненты в роли ключей словаря............................................................................195 7.2.6. Коллекции во встраиваемых компонентах.................................................................197 7.3. Отображение связей между сущностями............................................................................198 7.3.1. Самая простая связь............................................................................................................199 7.3.2. Определение двунаправленной связи..........................................................................200
Содержание 9 7.3.3. Каскадная передача состояния.......................................................................................202 7.4. Резюме..............................................................................................................................................209
Глава 8. Продвинутые приемы отображения связей между сущностями. ..............................................................................................................211 8.1. Связи один к одному....................................................................................................................212 8.1.1. Общий первичный ключ...................................................................................................212 8.1.2. Генератор внешнего первичного ключа........................................................................215 8.1.3. Соединение с помощью столбца внешнего ключа...................................................218 8.1.4. Использование таблицы соединения............................................................................220 8.2. Связь один ко многим..................................................................................................................222 8.2.1. Применение контейнеров в связях один ко многим.................................................223 8.2.2. Однонаправленное и двунаправленное отображения списка..............................224 8.2.3. Необязательная связь один ко многим с таблицей соединения............................227 8.2.4. Связь один ко многим во встраиваемых классах........................................................229 8.3. Тройные связи и связи многие ко многим.............................................................................231 8.3.1. Однонаправленные и двунаправленные связи многие ко многим.......................232 8.3.2. Связь многие ко многим с промежуточной сущностью...........................................234 8.3.3. Тройные связи с компонентами......................................................................................239 8.4. Связи между сущностями с использованием словарей..................................................242 8.4.1. Связь один ко многим со свойством для ключа..........................................................242 8.4.2. Тройное отношение вида ключ/значение...................................................................243 8.5. Резюме..............................................................................................................................................245
Глава 9. Сложные и унаследованные схемы................................................246 9.1. Улучшаем схему базы данных..................................................................................................247 9.1.1. Добавление вспомогательных объектов базы данных............................................248 9.1.2. Ограничения SQL................................................................................................................251 9.1.3. Создание индексов..............................................................................................................258 9.2. Унаследованные первичные ключи......................................................................................259 9.2.1. Отображение естественных первичных ключей.......................................................259 9.2.2. Отображение составных первичных ключей.............................................................260 9.2.3. Внешние ключи внутри составных первичных ключей.........................................262 9.2.4. Внешний ключ, ссылающийся на составной первичный ключ...........................266 9.2.5. Внешние ключи, ссылающиеся на непервичные ключи........................................267 9.3. Отображение свойств во вторичные таблицы...................................................................268 9.4. Резюме..............................................................................................................................................270
Часть III. Транзакционная обработка данных..............................................271 Глава 10. Управление данными. ...............................................................................272 10.1. Жизненный цикл хранения...................................................................................................273 10.1.1. Состояния экземпляров сущностей..........................................................................273
10 Содержание 10.1.2. Контекст хранения...........................................................................................................275 10.2. Интерфейс EntityManager.....................................................................................................277 10.2.1. Каноническая форма единицы работы.....................................................................277 10.2.2. Сохранение данных.........................................................................................................279 10.2.3. Извлечение и модификация хранимых данных....................................................280 10.2.4. Получение ссылки на объект.......................................................................................282 10.2.5. Переход данных во временное состояние................................................................283 10.2.6. Изменение данных в памяти........................................................................................285 10.2.7. Репликация данных.........................................................................................................285 10.2.8. Кэширование в контексте хранения..........................................................................286 10.2.9. Выталкивание контекста хранения...........................................................................288 10.3. Работа с отсоединенным состоянием...........................................................................289 10.3.1. Идентичность отсоединенных экземпляров..........................................................289 10.3.2. Реализация метода проверки равенства...................................................................292 10.3.3. Отсоединение экземпляров сущностей....................................................................295 10.3.4. Слияние экземпляров сущностей..............................................................................296 10.4. Резюме..........................................................................................................................................298
Глава 11. Транзакции и многопоточность.......................................................299 11.1. Основы транзакций..................................................................................................................300 11.1.1. Атрибуты ACID................................................................................................................300 11.1.2. Транзакции в базе данных и системные транзакции...........................................300 11.1.3. Программные транзакции с JTA.................................................................................301 11.1.4. Обработка исключений..................................................................................................303 11.1.5. Декларативное определение границ транзакции..................................................306 11.2. Управление параллельным доступом.................................................................................307 11.2.1. Многопоточность на уровне базы данных...............................................................307 11.2.2. Оптимистическое управление параллельным доступом...................................313 11.2.3. Явные пессимистические блокировки.....................................................................322 11.2.4. Как избежать взаимоблокировок...............................................................................325 11.3. Доступ к данным вне транзакции.........................................................................................327 11.3.1. Чтение данных в режиме автоматического подтверждения.............................328 11.3.2. Создание очереди изменений......................................................................................330 11.4. Резюме...........................................................................................................................................332
Глава 12. Планы извлечения, стратегии и профили.............................333 12.1. Отложенная и немедленная загрузка................................................................................334 12.1.1. Прокси-объекты................................................................................................................335 12.1.2. Отложенная загрузка хранимых коллекций...........................................................339 12.1.3. Реализация отложенной загрузки путем перехвата вызовов...........................342 12.1.4. Немедленная загрузка коллекций и ассоциаций..................................................345 12.2. Выбор стратегии извлечения................................................................................................347 12.2.1. Проблема n + 1 выражений SELECT........................................................................347
Содержание 11 12.2.2. Проблема декартова произведения............................................................................348 12.2.3. Массовая предварительная выборка данных.........................................................351 12.2.4. Предварительное извлечение коллекций с помощью подзапросов...............354 12.2.5. Отложенное извлечение с несколькими выражениями SELECT...................355 12.2.6. Динамическое немедленное извлечение..................................................................356 12.3. Профили извлечения...............................................................................................................358 12.3.1. Определение профилей извлечения Hibernate......................................................359 12.3.2. Графы сущностей..............................................................................................................360 12.4. Резюме..........................................................................................................................................364
Глава 13. Фильтрация данных. ...................................................................................365 13.1. Каскадная передача изменений состояния......................................................................366 13.1.1. Доступные способы каскадирования........................................................................367 13.1.2. Транзитивное отсоединение и слияние....................................................................367 13.1.3. Каскадное обновление....................................................................................................370 13.1.4. Каскадная репликация...................................................................................................372 13.1.5. Глобальное каскадное сохранение..............................................................................373 13.2. Прием и обработка событий............................................................................................374 13.2.1. Приемники событий JPA и обратные вызовы........................................................374 13.2.2. Реализация перехватчиков Hibernate.......................................................................378 13.2.3. Базовый механизм событий..........................................................................................383 13.3. Аудит и версионирование с помощью Hibernate Envers.............................................384 13.3.1. Включение ведения журнала аудита.........................................................................384 13.3.2. Ведение аудита..................................................................................................................386 13.3.3. Поиск версий......................................................................................................................387 13.3.4. Получение архивных данных.......................................................................................388 13.4. Динамическая фильтрация данных....................................................................................391 13.4.1. Создание динамических фильтров.............................................................................392 13.4.2. Применение фильтра.......................................................................................................392 13.4.3. Активация фильтра..........................................................................................................393 13.4.4. Фильтрация коллекций..................................................................................................394 13.5. Резюме..........................................................................................................................................395
Часть IV. Создание запросов.......................................................................................397 Глава 14. Создание и выполнение запросов. ..............................................398 14.1. Создание запросов....................................................................................................................399 14.1.1. Интерфейсы запросов JPA............................................................................................399 14.1.2. Результаты типизированных запросов.....................................................................402 14.1.3. Интерфейсы Hibernate для работы с запросами...................................................402 14.2. Подготовка запросов................................................................................................................404 14.2.1. Защита от атак на основе внедрения SQL-кода.....................................................404 14.2.2. Связывание именованных параметров.....................................................................405
12 Содержание 14.2.3. Связывание позиционных параметров.....................................................................406 14.2.4. Постраничная выборка больших наборов с результатами.................................407 14.3. Выполнение запросов..............................................................................................................409 14.3.1. Извлечение полного списка результатов.................................................................409 14.3.2. Получение единичных результатов...........................................................................409 14.3.3. Прокрутка с помощью курсоров базы данных.......................................................411 14.3.4. Обход результатов с применением итератора........................................................412 14.4. Обращение к запросам по именам и их удаление из программного кода.............413 14.4.1. Вызов именованных запросов......................................................................................414 14.4.2. Хранение запросов в метаданных XML...................................................................414 14.4.3. Хранение запросов в аннотациях................................................................................416 14.4.4. Программное создание именованных запросов.....................................................416 14.5. Подсказки для запросов.........................................................................................................417 14.5.1. Установка предельного времени выполнения........................................................418 14.5.2. Установка режима выталкивания контекста хранения.......................................419 14.5.3. Установка режима только для чтения.......................................................................419 14.5.4. Определение количества одновременно извлекаемых записей.......................420 14.5.5. Управление комментариями SQL...............................................................................420 14.5.6. Подсказки для именованных запросов.....................................................................421 14.6. Резюме..........................................................................................................................................422
Глава 15. Языки запросов...............................................................................................424 15.1. Выборка........................................................................................................................................425 15.1.1. Назначение псевдонимов и определение корневых источников запроса...............................................................................................................................................426 15.1.2. Полиморфные запросы...................................................................................................427 15.2. Ограничения...............................................................................................................................428 15.2.1. Выражения сравнения....................................................................................................430 15.2.2. Выражения с коллекциями...........................................................................................434 15.2.3. Вызовы функций..............................................................................................................435 15.2.4. Упорядочение результатов запроса............................................................................438 15.3. Проекции.....................................................................................................................................439 15.3.1. Проекция сущностей и скалярных значений.........................................................439 15.3.2. Динамическое создание экземпляров.......................................................................441 15.3.3. Извлечение уникальных результатов........................................................................443 15.3.4. Вызов функций в проекциях........................................................................................443 15.3.5. Агрегирующие функции................................................................................................446 15.3.6. Группировка данных........................................................................................................447 15.4. Соединения.................................................................................................................................449 15.4.1. Соединения в SQL...........................................................................................................449 15.4.2. Соединение таблиц в JPA..............................................................................................452 15.4.3. Неявные соединения по связи.....................................................................................452 15.4.4. Явные соединения............................................................................................................454
Содержание 13 15.4.5. Динамическое извлечение с помощью соединений.............................................456 15.4.6. Тета-соединения................................................................................................................460 15.4.7. Сравнение идентификаторов.......................................................................................461 15.5. Подзапросы.................................................................................................................................463 15.5.1. Коррелированные и некореллированные подзапросы........................................463 15.5.2. Кванторы.............................................................................................................................464 15.6. Резюме..........................................................................................................................................466
Глава 16. Дополнительные возможности запросов.............................467 16.1. Преобразование результатов запросов..............................................................................467 16.1.1. Получение списка списков............................................................................................469 16.1.2. Получение списка словарей..........................................................................................469 16.1.3. Отображение атрибутов в свойства компонента JavaBean................................470 16.1.4. Создание преобразователя ResultTransformer.......................................................471 16.2. Фильтрация коллекций..........................................................................................................472 16.3. Интерфейс запросов на основе критериев в Hibernate................................................475 16.3.1. Выборка и упорядочение...............................................................................................475 16.3.2. Ограничения......................................................................................................................476 16.3.3. Проекция и агрегирование............................................................................................478 16.3.4. Соединения.........................................................................................................................479 16.3.5. Подзапросы.........................................................................................................................481 16.3.6. Запросы по образцу.........................................................................................................482 16.4. Резюме..........................................................................................................................................484
Глава 17. Настройка SQL-запросов.......................................................................485 17.1. Назад к JDBC.............................................................................................................................486 17.2. Отображение результатов SQL-запросов.........................................................................488 17.2.1. Проекции в SQL-запросах.............................................................................................489 17.2.2. Отображение в классы сущностей.............................................................................490 17.2.3. Настройка отображения запросов..............................................................................492 17.2.4. Размещение обычных запросов в отдельных файлах..........................................504 17.3. Настройка операций CRUD..................................................................................................509 17.3.1. Подключение собственных загрузчиков..................................................................509 17.3.2. Настройка операций создания, изменения, удаления.........................................510 17.3.3. Настройка операций над коллекциями....................................................................512 17.3.4. Немедленное извлечение в собственном загрузчике...........................................514 17.4. Вызов хранимых процедур....................................................................................................517 17.4.1. Возврат результата запроса...........................................................................................518 17.4.2. Возврат нескольких результатов и количества изменений...............................519 17.4.3. Передача входных и выходных аргументов............................................................521 17.4.4. Возвращение курсора......................................................................................................524 17.5. Применение хранимых процедур для операций CRUD.............................................526 17.5.1. Загрузчик, вызывающий процедуру..........................................................................526
14 Содержание 17.5.2. Использование процедур в операциях CUD..........................................................527 17.6. Резюме..........................................................................................................................................529
Часть V. Создание приложений.................................................................................531 Глава 18. Проектирование клиент-серверных приложений.........532 18.1. Разработка уровня хранения.................................................................................................533 18.1.1. Обобщенный шаблон «объект доступа к данным»....................................................535 18.1.2. Реализация обобщенных интерфейсов.....................................................................537 18.1.3. Реализация интерфейсов DAO....................................................................................539 18.1.4. Тестирование уровня хранения...................................................................................541 18.2. Создание сервера без состояния..........................................................................................543 18.2.1. Редактирование информации о товаре.....................................................................543 18.2.2. Размещение ставки..........................................................................................................546 18.2.3. Анализ приложения без состояния............................................................................550 18.3. Разработка сервера с сохранением состояния................................................................552 18.3.1. Редактирование информации о товаре.....................................................................553 18.3.2. Анализ приложений с сохранением состояния.....................................................558 18.4. Резюме..........................................................................................................................................561
Глава 19. Создание веб-приложений..................................................................562 19.1. Интеграция JPA и CDI............................................................................................................563 19.1.1. Создание экземпляра EntityManager........................................................................563 19.1.2. Присоединение экземпляра EntityManager к транзакциям..............................565 19.1.3. Внедрение экземпляра EntityManager......................................................................565 19.2. Сортировка и постраничная выборка данных................................................................567 19.2.1. Реализация постраничной выборки с помощью смещения или поиска.......567 19.2.2. Реализация постраничной выборки в уровне хранения.....................................570 19.2.3. Постраничная выборка...................................................................................................576 19.3. Создание JSF-приложений....................................................................................................577 19.3.1. Службы с областью видимости запроса...................................................................578 19.3.2. Службы с областью видимости диалога...................................................................581 19.4. Сериализация данных предметной модели.....................................................................590 19.4.1. Создание JAX-RS-службы.............................................................................................591 19.4.2. Применение JAXB-отображений................................................................................592 19.4.3. Сериализация прокси-объектов Hibernate..............................................................595 19.5. Резюме..........................................................................................................................................598
Глава 20. Масштабирование Hibernate. ............................................................599 20.1. Массовые и пакетные операции обработки данных.....................................................600 20.1.1. Массовые операции в запросах на основе критериев и JPQL..........................600 20.1.2. Массовые операции в SQL............................................................................................605 20.1.3. Пакетная обработка данных.........................................................................................606
Содержание 15 20.1.4. Интерфейс StatelessSession . ........................................................................................610 20.2. Кэширование данных..............................................................................................................612 20.2.1. Архтектура общего кэша в Hibernate........................................................................613 20.2.2. Настройка общего кэша.................................................................................................618 20.2.3. Кэширование коллекций и сущностей.....................................................................619 20.2.4. Проверка работы разделяемого кэша........................................................................623 20.2.5. Установка режимов кэширования..............................................................................625 20.2.6. Управление разделяемым кэшем................................................................................627 20.2.7. Кэш результатов запросов.............................................................................................627 20.3. Резюме..........................................................................................................................................630
Библиография............................................................................................................................631
Предисловие к первому изданию Реляционные базы данных, бесспорно, составляют основу современного предприятия. В то время как современные языки программирования, включая Java, обеспечивают интуитивное, объектно-ориентированное представление бизнессущностей уровня приложения, данные, лежащие в основе этих сущностей, имеют выраженную реляционную природу. Кроме того, главное преимущество реляционной модели перед более ранней навигационной моделью, а также поздними моделями объектно-ориентированных баз данных – в том, что она изначально внутренне независима от программных взаимодействий и представления данных на уровне приложения. Было предпринято немало попыток для объединения реляционной и объектно-ориентированной технологий или замещения одной на другую, но пропасть между ними остается сегодня одним из непреложных фактов. Именно эту задачу – обеспечить связь между реляционными данными и Javaобъектами – решает Hibernate при помощи своего подхода к реализации объектнореляционного отображения (Object/Relational Mapping, ORM). Hibernate решает данную задачу очень прагматичным, ясным и реалистичным способом. Как показывают Кристиан Бауэр (Christian Bauer) и Гэвин Кинг (Gavin King) в этой книге, эффективное использование технологии ORM в любом бизнес-окружении, кроме простейшего, требует понимания особенностей работы механизма, осуществляющего посредничество между реляционными данными и объектами. То есть разработчик должен понимать требования к своему приложению и к данным, владеть языком SQL, знать структуры реляционных хранилищ, а также иметь представление о возможных способах оптимизации, предоставляемых реляционными технологиями. Hibernate не только предоставляет полностью рабочее решение, непосредственно удовлетворяющее этим требованиям, но и гибкую и настраиваемую архитектуру. Разработчики Hibernate изначально сделали фреймворк модульным, расширяемым и легко настраиваемым под нужды пользователя. В результате через несколько лет после выхода первой версии фреймворк Hibernate быстро стал – и заслуженно – одной из ведущих реализаций ORMтехнологий для разработчиков корпоративных приложений. В этой книге представлен подробный обзор фреймворка Hibernate. Описывается, как использовать его возможности отображения типов и средства моделирования ассоциаций и наследования; как эффективно извлекать объекты, используя
17 язык запросов Hibernate; как настраивать Hibernate для работы в управляемом и неуправляемом окружениях; как использовать его инструментарий. Кроме того, на протяжении всей книги авторы приоткрывают проблемы ORM и проектные решения, легшие в основу Hibernate. Это дает читателю глубокое понимание эффективного использования ORM как корпоративной технологии. Данная книга является подробным руководством по Hibernate и объектно-реляционному отображению в корпоративных вычислениях. Линда Демишель (Linda DeMichiel) Ведущий архитектор, Enterprise Javabeans Sun Microsystems Ноябрь 2012
Введение Это наша третья книга о Hibernate, проекте с открытым исходным кодом, которому почти 15 лет. Согласно недавнему опросу, Hibernate оказался в числе пяти самых популярных инструментов, которыми Java-разработчики пользуются каждый день. Это говорит о том, что базы данных SQL являются предпочтительной технологией для надежного хранения и управления данными, особенно в области разработки корпоративных приложений на Java. Также это является доказательством качества доступных спецификаций и инструментов, упрощающих запуск проектов, оценку и снижение рисков при создании крупных и сложных приложений. Сейчас доступны пятая версия Hibernate и вторая версия спецификации Java Persistence API (JPA), которую реализует Hibernate. Ядро Hibernate или то, что сейчас называется объектно-реляционным отображением (Object/Relational Mapping, ORM), уже долгое время является развитой технологией, и на протяжении многих лет было сделано множество мелких улучшений. Другие связанные проекты, такие как Hibernate Search, Hibernate Bean Validation и недавнее объектно-сеточное отображение (Object/Grid Mapping, OGM), привносят новые инновационные решения, которые превращают Hibernate в полноценный набор инструментов для решения широкого спектра задач управления данными. Когда мы работали над предыдущим изданием этой книги, с Hibernate происходили важные изменения: в силу своего органичного развития, влияния со стороны сообщества разработчиков свободного ПО и повседневных требований Javaразработчиков Hibernate пришлось стать более формальным и реализовать первую версию спецификации JPA (Java Persistence API). Поэтому предыдущее издание получилось громоздким, так как многие примеры мы были вынуждены демонстрировать с использованием старого и нового, стандартизированного, подходов. В настоящий момент этот расхождение практически исчезло, и мы можем в первую очередь опираться на стандартизированный прикладной программный интерфейс (API) и архитектуру Java Persistence. Также в этом издании мы обсудим множество выдающихся особенностей Hibernate. Хотя объем книги уменьшился, по сравнению с предыдущим изданием, мы использовали это место для многочисленных новых примеров. Мы также рассмотрим, как JPA вписывается в общую картину Java EE и как ваше приложение может интегрировать Bean Validation, EJB, CDI и JSF. Пусть это новое издание станет путеводителем для вашего первого проекта Hibernate. Мы надеемся, что оно заменит предыдущее издание в качестве настольного справочного материала по Hibernate.
Благодарности Мы не смогли бы написать эту книгу без помощи многих людей. Палак Матур (Palak Mathur) и Кристиан Альфано (Christian Alfano) проделали отличную работу, будучи техническими рецензентами нашей книги; спасибо вам за долгие часы, потраченные на редактирование наших «поломанных» примеров кода. Мы также хотели бы поблагодарить наших рецензентов за потраченное время и неоценимую обратную связь в процессе разработки: Криса Бакара (Chris Bakar), Гаурава Бхардвая (Gaurav Bhardwaj), Якоба Босму (Jacob Bosma), Хосе Диаза (José Diaz), Марко Гамбини (Marco Gambini), Серхио Фернандеса Гонсалеса (Sergio Fernandez Gonzalez), Джерри Гуднафа (Jerry Goodnough), Джона Гриффина (John Griffin), Стефана Хеффнера (Stephan Heffner), Чеда Джонстона (Chad Johnston), Кристофа Мартини (Christophe Martini), Робби О’Коннора (Robby O’Connor), Антони Патрисио (Anthony Patricio) и Дениса Ванга (Denis Wang). Издатель из Manning Марьян Бэйс (Marjan Bace) снова собрала отличную команду в Manning: Кристина Тэйлор (Christina Taylor) редактировала нашу сырую рукопись, превратив ее в настоящую книгу. Тиффани Тэйлор (Tiffany Taylor) нашла все опечатки, сделав книгу читаемой. Дотти Мариско (Dottie Marisco), ответственная за верстку, придала книге ее прекрасный вид. Мэри Пиргис (Mary Piergies) координировала и организовывала весь процесс. Мы благодарим всех вас за то, что работали с нами. Наконец, отдельное спасибо Линде Демишель (Linda DeMichiel) за предисловие к первому изданию.
Гэри Грегори (Gary Gregory) Я хотел бы поблагодарить моих родителей за то, что помогли мне начать это путешествие, дали мне прекрасное образование и свободу выбирать свой путь. Я бесконечно благодарен своей жене Лори и моему сыну Александру за то, что они предоставили мне время на завершение еще одного проекта – моей третьей книги. В процессе работы я учился и сотрудничал с действительно выдающимися личностями, такими как Джордж Босворт (George Bosworth), Ли Брайзахер (Lee Breisacher), Кристофер Хэнсон (Christopher Hanson), Дебора Льюис (Deborah Lewis) и многими другими. Мой тесть, Бадди Мартин (Buddy Martin), заслуживает особого упоминания за то, что делился своей мудростью и проницательностью во время долгих бесед и рассказов историй, накопленных за те десятилетия, что он писал о спорте (вперед, «Аллигаторы»!). Я всегда нахожу вдохновение в му-
20 Благодарности зыке, особенно в музыке следующих исполнителей: Wilco (Impossible Germany), Tom Waits (Blue Valentine), Donald Fagen (The Night-fly, A just machine to make big decisions/Programmed by fellows with compassion and vision), David Lindley и Баха. Наконец, я благодарю своего соавтора Кристиана Бауэра (Christian Bauer) за то, что делился со мной знаниями, и всех сотрудников издательства Manning за поддержку, профессионализм и доброжелательные отзывы. Отдельное спасибо Тиффани Тэйлор (Tiffany Taylor) из Manning за то, что придала книге отличный вид. Дон Вэннер (Don Wanner), спасибо, точка.
Об этой книге Эта книга является и руководством, и справочником по Hibernate и Java Persistence. Если вы новичок в Hibernate, рекомендуем читать, начиная с главы 1, и опробовать все примеры с «Hello World» в главе 2. Если вы использовали старую версию Hibernate, прочитайте бегло две первые главы, чтобы получить общее представление, и переходите к середине главы 3. Там, где необходимо, мы сообщим, если конкретный раздел или тема являются дополнительными или справочными, которые можно пропустить при первом чтении.
Структура книги Эта книга состоит из пяти больших частей. В части I «Начинаем работать с ORM» мы обсудим основы объектно-реляционного отображения. Пройдемся по практическому руководству, чтобы помочь вам создать первый проект с Hibernate. Рассмотрим проектирование Java-приложений с точки зрения создания моделей предметной области и познакомимся с вариантами определения метаданных для объектно-реляционного отображения. В части II «Стратегии отображения» рассказывается о классах Java и их свойствах, а также об их отображении в таблицы и столбцы SQL. Мы рассмотрим все основные и продвинутые способы отображения в Hibernate и Java Persistence. Покажем, как работать с наследованием, коллекциями и сложными ассоциациями классов. В заключение обсудим интеграцию с существующими схемами баз данных, а также особенно запутанные стратегии отображения. Часть III «Транзакционная обработка данных» посвящена загрузке и сохранению данных с помощью Hibernate и Java Persistence. Мы представим программные интерфейсы, способы создания транзакционных приложений и то, как эффективнее загружать данные из базы данных в Hibernate. В части IV «Создание запросов» мы познакомимся с механикой извлечения данных и детально рассмотрим язык запросов и прикладной программный интерфейс (API). Не все главы данного раздела написаны как руководство; мы рассчитываем, что вы будете часто заглядывать в эту часть книги во время разработки приложений для поиска решений проблем с конкретными запросами. В части V «Разработка приложений» мы обсудим проектирование и разработку многоуровневых приложений баз данных на Java. Обсудим наиболее распространенные шаблоны проектирования, используемые вместе с Hibernate, такие как «Объект доступа к данным» (Data Access Object, DAO). Вы увидите, как можно легко протестировать приложение, использующее Hibernate, и познакомитесь с другими распространенными приемами, используемыми при работе с инструментами объектно-реляционного отображения в веб-приложениях или клиентсерверных приложениях в целом.
22 Об этой книге
Кому адресована эта книга? Читатели данной книги должны быть знакомы с основами объектно-ориентированного программирования и иметь практические навыки его применения. Чтобы понять примеры приложений, вы должны быть знакомы с языком программирования Java и унифицированным языком моделирования (Unified Modeling Language, UML). В первую очередь книга адресована Java-программистам, работающим с базами данных SQL. Мы покажем, как повысить продуктивность при работе с ORM. Если вы – разработчик баз данных, эта книга может отчасти послужить вам введением в объектно-ориентированную разработку программного обеспечения. Если вы администратор баз данных (DBA), вас наверняка заинтересует влияние ORM на производительность, а также способы настройки СУБД SQL и уровня хранения данных для достижения желаемой производительности. Доступ к данным является узким местом любого Java-приложения, поэтому проблемам производительности в данной книге уделяется повышенное внимание. Многие DBA, по понятным причинам, испытывают тревогу, не веря в высокую производительность автоматически сгенерированного кода SQL; мы постараемся развеять эти страхи, а также указать случаи, когда в приложениях не следует использовать автоматического доступа к данным. Вы почувствуете себя свободнее, когда поймете, что мы не считаем ORM лучшим решением для каждой проблемы.
Соглашения об оформлении программного кода Эта книга изобилует примерами, включающими всевозможные артефакты Hibernate-приложений: Java-код, файлы конфигурации Hibernate и XML-файлы с метаданными отображений. Исходный код в листингах или тексте выделен моноширинным шрифтом, как этот, чтобы отделить его от остального текста. Кроме того, имена Java-методов, параметры компонентов, свойства объектов, а также XML-элементы и их атрибуты в тексте представлены с использованием моноширинного шрифта. Листинги на Java, XML и HTML могут быть объемными. Во многих случаях исходный код (доступный онлайн) был переформатирован; мы добавили разделители строк и переделали отступы, чтобы уместить их по ширине книжных страниц. В редких случаях, когда этого оказалось недостаточно, в листинги были добавлены символы продолжения строки (➥). Также из многих листингов, описываемых в тексте, мы убрали комментарии. Некоторые листинги сопровождают аннотации, выделяющие важные понятия. В других случаях используются пронумерованные маркеры, отсылающие к пояснениям в тексте, следующим за листингами.
Загрузка исходного кода Hibernate – это проект с открытым исходным кодом, распространяемым на условиях лицензии Lesser GNU Public Lisence. Инструкции по загрузке модулей Hibernate в виде исходных или двоичных кодов доступны на сайте проекта: http://
авторах 23 hibernate.org/. Исходный код всех примеров в данной книге доступен на http:// jpwh.org. Код примеров можно также загрузить с сайта издательства https://www. manning.com/books/java-persistence-with-hibernate-second-edition.
Автор онлайн Приобретая книгу «Java Persistence API и Hibernate», вы получаете бесплатный доступ на частный веб-форум издательства Manning Publications, где вы сможете оставлять отзывы о книге, задавать технические вопросы и получать помощь от авторов и других пользователей. Чтобы получить доступ к форуму и зарегистрироваться на нем, откройте в браузере страницу https://www.manning.com/books/ java-persistence-with-hibernate-second-edition. На этой странице «Author Online» (Автор в сети) описывается, как попасть на форум после регистрации, какие виды помощи доступны и правила поведения на форуме. Издательство Manning обязуется предоставить своим читателям место встречи, где может состояться содержательный диалог между отдельными читателями и между читателями и автором. Но со стороны автора отсутствуют какие-либо обязательства уделять форуму какое-то определенное внимание – его присутствие на форуме остается добровольным (и неоплачиваемым). Мы предлагаем задавать автору стимулирующие вопросы, чтобы его интерес не угасал! Форум и архивы предыдущих дискуссий будут оставаться доступными, пока книга продолжает издаваться.
Об авторах Кристиан Бауэр (Christian Bauer) – член коллектива разработчиков Hibernate; он работает инструктором и консультантом. Гэвин Кинг (Gavin King) – основатель проекта Hibernate и член первоначального состава экспертной группы по Java Persistence (JSR 220). Он также участвовал в работе по стандартизации CDI (JSR 299). В настоящее время Гэвин разрабатывает новый язык программирования Ceylon. Гэри Грегори (Gary Gregory) является главным инженером в Rocket Software, где работает над серверами приложений и интеграцией с устаревшими системами. Еще он соавтор книг издательства Manning: «JUnit in Action» и «Spring Batch in Action», а также член комитетов по руководству проектом (Project Management Commitee) в различных проектах в Apache Software Foundation: Commons, HttpComponents, Logging Services и Xalan.
Об изображении на обложке Иллюстрация на обложке книги «Java Persistence API и Hibernate, второе издание» взята из сборника костюмов Османской империи, изданного 1 января 1802 г. Вильямом Миллером (William Miller) в Old Bond Street, Лондон. Титульный лист сборника утерян, поэтому мы не смогли точно определить дату его выхода. В содержании книги изображения описаны на английском и французском языках, и под каждой иллюстрацией приводятся имена двух художников, которые трудились над ней. Не сомневаемся, что все они, несомненно, были бы удивлены, узнав, что их творчество украсит обложку книги по программированию... 200 лет спустя. Рисунки из сборника костюмов Османской империи, так же как и другие иллюстрации, появляющиеся на наших обложках, возрождают богатство и разно образие традиций в одежде двухсотлетней давности. Они напоминают о чувствах уединенности и удаленности этого периода – и любого другого исторического периода, кроме нашего гиперкинетического настоящего. Манеры одеваться сильно изменились с тех пор, а своеобразие каждого региона, такое яркое в то время, давно поблекло. Сейчас бывает трудно различить обитателей разных континентов. Возможно, если смотреть на это оптимистично, мы обменяли культурное и ви зуальное разнообразие на более разнообразную частную жизнь или более разно образную интеллектуальную и техническую жизнь. Мы в Manning высоко ценим изобретательность, инициативу и, конечно, радость от компьютерного бизнеса с книжными обложками, основанными на разнообразии жизни в разных регионах два века назад, которое оживает благодаря картинкам из этой коллекции.
Часть
I ils gDeta Billin ing tr S r: owne
НАЧИНАЕМ РАБОТАТЬ С ORM > LA ST NAM > > ME
d itCar Cred ng ing id : Lo mber : Str u cardN nth : String o expM r : String a expYe
unt Acco Bank g n id : Lo t : String n g accou me : Strin a bankn tring :S swift
В части I мы объясним, почему долговременное хранение объектов является та> ZIP Rпреодоления EE K > SS несколько стратегий Cдля этого несоответствия – в первую очередь CO T > DE объектно-реляционноеITYотображение (ORM). В главе 2 мы по шагам разберем с вами учебный пример с Hibernate и Java Persistence – программу «Hello World». После такой начальной подготовки в главе 3 вы узнаете, как проектировать и разрабатывать сложные предметные модели на Java и какие виды метаданных отображения доступны. После прочтения этой части книги вы поймете, для чего требуется ORM и как Hibernate и Java Persistence работают на практике. Вы создадите свой первый небольшой проект и подготовитесь к решению более сложных задач. Вы также узнаете, как представлять реальные бизнес-сущности в виде предметных моделей на Java, и выберете наиболее предпочтительный формат для работы с метаданными ORM.
ble > ID :price", timeout = 60, comment = "Custom SQL comment" ) }) package org.jpwh.model.querying;
Особенности Hibernate И наконец, подсказки можно определять для запросов в XML-файлах конфигурации Hibernate:
select i from Item i order by i.auctionEnd asc
14.6. Резюме Вы узнали, как создавать и выполнять запросы. Для создания и обработки результатов запросов используются интерфейсы JPA. Вы также познакомились с интерфейсами запросов Hibernate. Вы узнали, как готовить запросы к выполнению и защищаться от атак, связанных с внедрением кода SQL. Вы познакомились со связанными и позиционными параметрами, а также с постраничной обработкой больших результатов запросов.
Резюме 423 Чтобы не встраивать JPQL-запросов в исходные файлы на Java, можно присвоить им имена и переместить во внешние файлы. Вы видели, как вызывать именованные запросы, а также узнали способы их определения: в метаданных XML, в аннотациях и программно. Мы рассказали о подсказках для запросов, которые можно передавать в Hibernate: предельное время выполнения, доступ только на чтение, количество одновременно извлекаемых записей и комментарии SQL. На примере JPQL мы продемонстрировали, как задавать подсказки для запросов во внешних файлах.
Глава
15 Языки запросов
В этой главе: запросы JPQL и запросы на основе критериев; эффективное извлечение данных с помощью соединений; запросы и подзапросы для получения отчетов.
Создание запросов – наиболее интересный этап в разработке приложений баз данных. Для создания сложного запроса может потребоваться много времени, и он может оказать колоссальное влияние на производительность приложения. С другой стороны, писать запросы становится проще по мере приобретения опыта, и то, что сначала казалось сложным, становится простым после изучения различных языков запросов. В этой главе рассматриваются языки запросов, доступные в JPA: JPQL и API для запросов на основе критериев. Мы будем показывать примеры запросов, созданных с использованием обоих языков/API и возвращающих одинаковые результаты. Главные нововведения в JPA 2 • Добавлена поддержка операторов CASE, NULLIF и COALESCE, обладающих такой же семантикой, как и их аналоги в SQL. • В выборке и ограничении запроса можно выполнить приведение к более спе цифичному типу с помощью оператора TREAT. • В ограничениях и проекциях запросов можно вызывать произвольные SQLфункции. • Используя новое ключевое слово ON, можно добавлять дополнительные условия соединений. • В предложениях FROM подзапросов можно использовать соединения.
Мы рассчитываем, что во время разработки приложения вы будете неоднократно возвращаться к этой главе, используя ее в качестве руководства по синтаксису запросов. Вследствие этого наше изложение будет кратким и сопровождаться небольшими примерами кода для различных вариантов использования. Иногда мы будем упрощать части приложения CaveatEmptor ради удобочитаемости. Напри-
Выборка 425 мер, в сравнениях мы будем обращаться не к экземплярам MonetaryAmount, а к экземплярам BigDecimal. Для начала разберемся с терминологией, относящейся к запросам. Для определения источников данных используются выборки (selections), для отбора записей, удовлетворяющих определенным критериям, – ограничения (restrictions), и для перечисления выбираемых столбцов данных – проекции (projections). Вы увидите, что данная глава организована в том же порядке. В этой главе, говоря о запросах, мы, как правило, будем иметь в виду выражения SELECT: операции, извлекающие информацию из базы данных. JPA также поддерживает выражения UPDATE, DELETE, выражение INSERT ... SELECT в JPQL, запросы на основе критериев и различные реализации SQL, о которых мы расскажем в разделе 20.1. Мы не будем повторять здесь этих массовых операций и сосредоточимся только на выражениях SELECT. Для начала покажем несколько простых примеров выборок.
15.1. Выборка Во-первых, говоря выборка, мы не имеем в виду предложения запроса SELECT. Мы также не имеем в виду самого выражения SELECT. Мы говорим о выборе реляционной переменной, или, говоря на языке SQL, о предложении FROM. Оно описывает источник данных, или, проще говоря, таблицы, из которых будет производиться выборка. Используя имена классов вместо таблиц, в JPQL можно написать следующее: from Item
Следующий запрос (только предложение FROM) извлекает все экземпляры Item. Для него Hibernate сгенерирует такой SQL-запрос: select i.ID, i.NAME, ... from ITEM i
Эквивалентный запрос на основе критериев можно создать, передав имя сущности в метод from(): CriteriaQuery criteria = cb.createQuery(Item.class); criteria.from(Item.class);
Особенности Hibernate Hibernate может работать с запросами, содержащими лишь одно предложение FROM или критерий. К сожалению, ни JPQL, ни запросы на основе критериев, которые мы только что продемонстрировали, не являются переносимыми: они несовместимы с JPA. Спецификация JPA требует, чтобы запрос JPQL содержал предложение SELECT, а переносимые запросы на основе критериев вызывали метод select(). Это требует назначения псевдонимов и определения корневых источников запроса, что является нашей следующей темой.
426 Языки запросов
15.1.1. Назначение псевдонимов и определение корневых источников запроса Добавление предложения SELECT в запрос JPQL требует присваивания псевдонима (alias) классу в предложении FROM, чтобы можно было ссылаться на него в других предложениях запроса. select i from Item as i
После этого следующий запрос будет совместим с JPA. Как обычно, ключевое слово as является необязательным. Следующий запрос аналогичен предыдущему: select i from Item i
Извлекаемым экземплярам класса Item присваивается псевдоним i. Это похоже на объявление временной переменной в следующем коде на Java: for(Iterator i = result.iterator(); i.hasNext();) { Item item = (Item) i.next(); // ... }
Псевдонимы в запросах нечувствительны к регистру, поэтому запрос select iTm from Item itm также будет работать. Мы предпочитаем использовать короткие и простые псевдонимы; они должны быть уникальными в пределах запроса (или подзапроса). Переносимые запросы на основе критериев должны вызывать метод select(): CriteriaQuery criteria = cb.createQuery(); Root i = criteria.from(Item.class); criteria.select(i);
В большинстве примеров с использованием запросов на основе критериев мы будем убирать строку cb.createQuery(); она всегда выглядит одинаково. Каждый раз, встречая переменную criteria, знайте, что она получена с помощью вызова CriteriaBuilder#createQuery(). Как получить экземпляр CriteriaBuilder, рассказывалось в предыдущей главе. Экземпляр Root всегда ссылается на сущность. Позже вы увидите запросы с несколькими корневыми элементами. Этот запрос можно записать короче, если не создавать дополнительной переменной типа Root: criteria.select(criteria.from(Item.class));
Тип сущности также можно указать динамически, с помощью Metamodel API: EntityType entityType = getEntityType( em.getMetamodel(), "Item" ); criteria.select(criteria.from(entityType));
Выборка 427 Мы написали метод getEntityType() исключительно для удобства: он обходит все экземпляры коллекции Metamodel#getEntities() и находит сущность с соответствующим именем. Сущность Item не имеет подклассов, поэтому в следующем разделе мы рассмот рим полиморфные запросы.
15.1.2. Полиморфные запросы JPQL, как и подобает объектно-ориентированному языку запросов, поддерживает полиморфные запросы, извлекающие не только экземпляры класса, но и все экземпляры подклассов. Рассмотрим следующие запросы: select bd from BillingDetails bd criteria.select(criteria.from(BillingDetails.class));
Они возвращают все экземпляры типа BillingDetails, который сам является абстрактным классом. В этом случае каждый экземпляр будет относиться к одному из подтипов BillingDetails: CreditCard или BankAccount. Чтобы получить только экземпляры конкретного подкласса, можно написать следующий запрос: select cc from CreditCard cc criteria.select(criteria.from(CreditCard.class));
Класс, указанный в предложении FROM, не обязан быть отображаемым хранимым классом; подойдет любой класс. Следующий запрос возвращает все сохраненные объекты: select o from java.lang.Object o
Особенности Hibernate Да, вы можете выбрать записи из всех таблиц базы данных и загрузить их в память! Это работает и с интерфейсами – можно, к примеру, выбрать все сериали зуемые экземпляры: select s from java.io.Serializable s
Плохая новость в том, что JPA не стандартизует полиморфных запросов JPQL с использованием интерфейсов. Они работают только в Hibernate, но переносимое приложение должно ссылаться в предложении FROM лишь на отображаемые классы сущностей, такие как BillingDetails или CreditCard. Метод from() из API запросов на основе критериев принимает только типы отображаемых сущностей. Можно выполнить неполиморфный запрос, ограничив набор возвращаемых типов с помощью функции TYPE. Чтобы извлечь только экземпляры конкретного подкласса, можно выполнить такой запрос:
428 Языки запросов select bd from BillingDetails bd where type(bd) = CreditCard Root bd = criteria.from(BillingDetails.class); criteria.select(bd).where( cb.equal(bd.type(), CreditCard.class) );
Для параметризации этого запроса нужно добавить предложение IN с именованным параметром: select bd from BillingDetails bd where type(bd) in :types Root bd = criteria.from(BillingDetails.class); criteria.select(bd).where( bd.type().in(cb.parameter(List.class, "types")) );
Связывание аргумента с параметром запроса происходит во время передачи списка (List) возвращаемых типов: Query query = // ... query.setParameter("types", Arrays.asList(CreditCard.class, BankAccount.class));
Чтобы выбрать все экземпляры подкласса, кроме конкретного класса, можно выполнить следующий запрос: select bd from BillingDetails bd where not type(bd) = BankAccount Root bd = criteria.from(BillingDetails.class); criteria.select(bd).where( cb.not(cb.equal(bd.type(), BankAccount.class)) );
Полиморфизм в запросах применяется не только к конкретным классам, но и к полиморфным ассоциациям, как вы увидите далее. На этом мы заканчиваем описание первого этапа создания запросов – определение выборки. Мы определили таблицы, из которых будем извлекать данные. Далее вам может понадобиться уменьшить количество извлекаемых записей с помощью ограничения.
15.2. Ограничения Как правило, извлекать все экземпляры класса из базы данных не требуется. Поэтому вы должны уметь описывать критерии отбора данных, возвращаемых запросом. Такие описания мы называем ограничениями. В SQL и JPQL ограничения описываются с помощью предложения WHERE, а в API на основе критериев аналогичную функцию выполняет метод where().
Ограничения 429 Ниже показан пример типичного предложения WHERE, ограничивающего набор результатов только экземплярами Item с конкретным именем. select i from Item i where i.name = 'Foo' Root i = criteria.from(Item.class); criteria.select(i).where( cb.equal(i.get("name"), "Foo") );
Ограничение в запросе выражается в терминах поля name класса Item. Ниже показан код SQL, сгенерированный данным запросом: select i.ID, i.NAME, ... from ITEM i where i.NAME = 'Foo'
В выражениях и условиях можно использовать строковые литералы, заключая их в одиночные кавычки. Для литералов, представляющих дату, время и метки времени, нужно использовать синтаксис экранирования JDBC: ... where i.auctionEnd = {d '2013-26-06'}. Обратите внимание, что способ обработки такого литерала, а также различные варианты его написания определяются драйвером JDBC и СУБД. И не забудьте совет из предыдущей главы: не внедряйте непроверенных входных данных пользователя в строку запроса – используйте параметры. В JDBC также часто встречаются литералы true и false: select u from User u where u.activated = true Root u = criteria.from(User.class); criteria.select(u).where( cb.equal(u.get("activated"), true) );
В SQL (а также в JPQL и запросах на основе критериев) ограничения описываются с помощью троичной логики. Предложение WHERE – это логическое выражение, результатом которого может быть true, false или null. Что такое троичная логика? Запись будет добавлена в результат SQL-запроса тогда и только тогда, когда выражение в предложении WHERE вернет true. В языке Java результатом выражения nonNullObject == null является false, а null == null – true. В языке SQL оба выражения – NOT_NULL_COLUMN = null и null = null – вернут null, а не true. Поэтому для проверки выражения на равенство null в SQL приходится использовать дополнительные операторы IS NULL и IS NOT NULL. Троичная логика нужна для работы с выражениями со столбцами, которые могут содержать null. Интерпретируйте null не как специальную метку, а как обычное значение – это расширение знакомой двоичной логики реляционной модели в языке SQL. Hibernate поддерживает троичную логику с помощью троичных операторов как в JPQL, так и в запросах на основе критериев.
430 Языки запросов Рассмотрим наиболее распространенные операторы сравнения в логических выражениях, включая троичные операторы.
15.2.1. Выражения сравнения JQPL и API запросов на основе критериев поддерживают такой же набор операторов сравнения, как и SQL. Ниже представлено несколько примеров, которые должны быть знакомы всем, кто знает язык SQL. Следующий запрос возвращает все ставки с ценой в заданном диапазоне: select b from Bid b where b.amount between 99 and 110 Root b = criteria.from(Bid.class); criteria.select(b).where( cb.between( Нужно указать тип атрибута b.get("amount"), new BigDecimal("99"), new BigDecimal("110") Аргументы должны быть того же типа ) );
Запрос на основе критериев может показаться несколько странным; вероятно, вы нечасто видите параметризацию в середине выражений на Java. Метод Root#get() возвращает объект Path, описывающий путь к атрибуту сущности. Для обеспечения типобезопасности требуется явно указать тип атрибута для этого пути Path, как в выражении get("amount"). Два оставшихся аргумента метода between() должны иметь тот же тип, иначе сравнение будет бессмысленным или вовсе не скомпилируется. Следующий запрос возвращает все ставки с ценой выше заданной: select b from Bid b where b.amount > 100 Root b = criteria.from(Bid.class); criteria.select(b).where( cb.gt( b.get("amount"), new BigDecimal("100") ) );
Метод gt() работает только с подклассами Number; для других типов используйте метод greaterThan()
Метод gt() принимает только аргументы подтипов Number, таких как BigDecimal или Integer. Чтобы сравнить значения других типов, например Date, используйте метод greaterThan(): Root i = criteria.from(Item.class); criteria.select(i).where( cb.greaterThan( i.get("auctionEnd"), tomorrowDate ) );
Ограничения 431 Следующий запрос возвращает всех пользователей с именами «johndoe» и «janeroe». select u from User u where u.username in ('johndoe', 'janeroe') Root u = criteria.from(User.class); criteria.select(u).where( cb.in(u.get("username")) .value("johndoe") .value("janeroe") );
В ограничениях с перечислениями следует указывать полное квалифицированное значение: select i from Item i where i.auctionType = org.jpwh.model.querying.AuctionType.HIGHEST_BID Root i = criteria.from(Item.class); criteria.select(i).where( cb.equal( i.get("auctionType"), AuctionType.HIGHEST_BID ) );
Из-за того, что SQL использует троичную логику, проверка значений на равенство null требует специального подхода. JPQL предоставляет операторы IS [NOT] NULL, а в API запросов на основе критериев – методы isNull() и isNotNull(). В следующем запросе IS NULL и isNull() используются для поиска товаров без текущей цены: select i from Item i where i.buyNowPrice is null Root i = criteria.from(Item.class); criteria.select(i).where( cb.isNull(i.get("buyNowPrice")) );
Используя IS NOT NULL и isNotNull(), можно получить все товары с указанной текущей ценой: select i from Item i where i.buyNowPrice is not null Root i = criteria.from(Item.class); criteria.select(i).where( cb.isNotNull(i.get("buyNowPrice")) );
Оператор LIKE позволяет организовать поиск, в котором часть шаблона может быть произвольной; как и в SQL, для этого применяются символы % и _:
432 Языки запросов select u from User u where u.username like 'john%' Root u = criteria.from(User.class); criteria.select(u).where( cb.like(u.get("username"), "john%") );
Выражение john% истинно только для пользователей, имена которых начинаются со строки «john». Также с оператором LIKE можно использовать отрицание: select u from User u where u.username not like 'john%' Root u = criteria.from(User.class); criteria.select(u).where( cb.like(u.get("username"), "john%").not() );
Можно выполнить поиск всех вхождений подстроки, окружив искомую строку символами процента: select u from User u where u.username like '%oe%' Root u = criteria.from(User.class); criteria.select(u).where( cb.like(u.get("username"), "%oe%") );
Символ процента означает «произвольная последовательность символов»; подчеркивание – «произвольный символ». Если понадобится использовать литералы процента и подчеркивания, их можно экранировать любым символом: select i from Item i where i.name like 'Name\_with\_underscores' escape :escapeChar query.setParameter("escapeChar", "\\"); Root i = criteria.from(Item.class); criteria.select(i).where( cb.like(i.get("name"), "Name\\_with\\_underscores", '\\') );
Эти запросы найдут товар с именем имя_с_подчеркиваниями. В строках языка Java для экранирования используется символ \, который тоже нужно экранировать, поэтому в предыдущем примере этот символ удвоен. JPA также поддерживает арифметические выражения: select b from Bid b where (b.amount / 2) - 0.5 > 49 Root b = criteria.from(Bid.class); criteria.select(b).where( cb.gt( cb.diff( cb.quot(b.get("amount"), 2), 0.5
Ограничения 433
)
);
), 49
Выражения объединяются с помощью логических операторов (и группирующих скобок): select i from Item i where (i.name like 'Fo%' and i.buyNowPrice is not null) or i.name = 'Bar'/ Root i = criteria.from(Item.class); Predicate predicate = cb.and( cb.like(i.get("name"), "Fo%"), cb.isNotNull(i.get("buyNowPrice")) ); predicate = cb.or( predicate, cb.equal(i.get("name"), "Bar") ); criteria.select(i).where(predicate);
Особенности Hibernate Если нужно объединить все предикаты с помощью логического И, для этого мы выбираем более удобный API запросов на основе критериев: Root i = criteria.from(Item.class); criteria.select(i).where( cb.like(i.get("name"), "Fo%"), // и cb.isNotNull(i.get("buyNowPrice")) // и ... );
Ниже, в табл. 15.1, перечислены все операторы в порядке убывания приоритета. Таблица 15.1. Приоритет операторов JPQL
.
API запросов на основе критериев Н/А
+, -
neg()
Оператор JPQL
Описание Оператор в выражении, описывающем путь к атрибуту Унарный плюс или минус (все беззнаковые числовые значения считаются положительными)
434 Языки запросов Таблица 15.1 (окончание) Оператор JPQL *, / +, =, , , >=,
1 Root c = criteria.from(Category.class);
Ограничения 435 criteria.select(c).where( cb.gt( cb.size(c.get("items")), 1 ) );
Также можно найти категорию, содержащую конкретный товар: select c from Category c where :item member of c.items Root c = criteria.from(Category.class); criteria.select(c).where( cb.isMember( cb.parameter(Item.class, "item"), c.get("items") ) );
Для хранимых словарей определены дополнительные операторы key(), value() и entry(). Предположим, что каждый экземпляр Item (товар) имеет хранимый словарь встроенных объектов Image (изображений), как показано в разделе 7.2.4. Имена файлов изображений являются ключами словаря. Следующий запрос извлекает все экземпляры Image с именами, оканчивающимися на .jpg: select value(img) from Item i join i.images img where key(img) like '%.jpg'
Оператор value() возвращает значения из словаря Map, а оператор key() – набор ключей. Для получения записей из словаря – экземпляров Map.Entry – используйте оператор entry(). Рассмотрим далее оставшиеся функции, применимые не только к коллекциям.
15.2.3. Вызовы функций Возможность вызова функций в предложении WHERE – одна из самых сильных сторон языков запросов. Следующие запросы вызывают функцию lower() для поиска подстроки без учета регистра: select i from Item i where lower(i.name) like 'ba%' Root i = criteria.from(Item.class); criteria.select(i).where( cb.like(cb.lower(i.get("name")), "ba%") );
Все доступные функции перечислены в табл. 15.2. Запросы на основе критериев имеют аналогичные методы в CriteriaBuilder, лишь немного отличающиеся способом записи имен (используется «верблюжийРегистр» без подчеркиваний).
436 Языки запросов Таблица 15.2. Функции запросов JPA (перегруженные методы не показаны) Функция upper(s), lower(s) concat(s, s) current_date, current_time, current_timestamp substring(s, offset, length)
Область применения Строковые значения; возвращает строку Строковые значения; возвращает строку Возвращает дату и/или время на сервере управления базами данных Строковые значения (смещение начинается с 1); возвращает строку trim([[both|leading|trailing] Удаляет пробелы с обеих (both) сторон строки s, если не указан char [from]] s) иной символ char или не заданы другие правила; возвращает строку length(s) Строковое значение; возвращает число locate(search, s, offset) Возвращает позицию подстроки search в строке s, начиная поиск с позиции offset; возвращает числовое значение abs(n), sqrt(n), mod(dividend, Числовые значения; возвращает абсолютное значение того же divisor) типа, что и аргумент, квадратный корень как Double и остаток от деления как Integer treat(x as Type) Приведение типа к подтипу в ограничениях; например, когда нужно найти всех пользователей с кредитными картами, срок действия которых заканчивается в 2013: select u from User u where treat(u.billingDetails as CreditCard).expYear = '2013'. (Обратите внимание, что это необязательно делать в Hibernate. Он автоматически выполнит приведение типа к подтипу, если используется поле подкласса) size(c) Коллекция выражений; возвращает Integer или 0, когда коллекция пуста index(orderedCollection) Выражение, возвращающее коллекцию, отображаемую с помощью @OrderColumn; возвращает значение типа Integer, соответствующее позиции аргумента в коллекции. Например, запрос select i.name from Category c join c.items i where index(i) = 0 вернет названия для каждого первого товара в каждой категории
Особенности Hibernate Как показано в табл. 15.3, Hibernate поддерживает дополнительные функции для JPQL. Стандартный JPA API запросов на основе критериев не имеет аналогов этих функций. Таблица 15.3. Функции запросов Hibernate Функция bit_length(s) second(d), minute(d), hour(d), day(d), month(d), year(d) minelement(c), maxelement(c), minindex(c), maxindex(c), elements(c), indices(c) str(x)
Описание Возвращает количество бит в s Извлекает время и дату из аргумента, представляющего время Возвращает элемент коллекции или индекс для коллекций, поддерживающих индексирование (словарей, списков, массивов) Выполняет приведение аргумента к строковому типу
Ограничения 437 Большинство этих функций транслируется в соответствующие функции SQL, которые вы видели ранее. Также можно вызывать функции SQL, поддерживаемые вашей СУБД и не показанные здесь.
Особенности Hibernate Любая функция, встреченная в предложении WHERE запроса JPQL и неизвестная Hibernate, передается напрямую в базу данных в виде вызова функции SQL. Следующий запрос, например, вернет товары, аукцион для которых длился больше одного дня: select i from Item i where datediff('DAY', i.createdOn, i.auctionEnd) > 1
Здесь вызывается нестандартная функция datediff(), доступная в базе данных H2, возвращающая разницу в днях между датами создания товара и окончания аукциона для данного экземпляра Item. Такой синтаксис может использоваться только с Hibernate; в JPA для вызова произвольных функций SQL стандартизован следующий синтаксис: select i from Item i where function('DATEDIFF', 'DAY', i.createdOn, i.auctionEnd) > 1
Первый аргумент функции function() – имя вызываемой функции SQL в одинарных кавычках. За ним должны следовать необходимые операнды, если имеются. Такой же запрос, но на основе критериев: Root i = criteria.from(Item.class); criteria.select(i).where( cb.gt( cb.function( "DATEDIFF", Integer.class, cb.literal("DAY"), i.get("createdOn"), i.get("auctionEnd") ), 1 ) );
Аргумент Integer.class определяет тип возвращаемого значения функции datediff(), но здесь он бесполезен, поскольку результат вызова функции в ограничении не возвращается.
438 Языки запросов Вызов функции в предложении SELECT передаст возвращаемое значение на сторону Java; в предложении SELECT также можно вызывать произвольные SQLфункции базы данных. Но, прежде чем говорить о проекциях, мы сначала узнаем, как упорядочивать результаты.
15.2.4. Упорядочение результатов запроса В любом языке запросов есть механизм упорядочения результатов. В JPQL эту роль играет предложение ORDER BY, как в SQL. Следующий запрос найдет всех пользователей и упорядочит их по именам (по умолчанию в порядке возврастания): select u from User u order by u.username
Возрастание или убывание определяется ключевым словом asc или desc: select u from User u order by u.username desc
В API запросов на основе критериев порядок сортировки должен указываться обязательно, с помощью функции asc() или desc(): Root u = criteria.from(User.class); criteria.select(u).orderBy( cb.desc(u.get("username")) );
Упорядочивать можно по нескольким атрибутам: select u from User u order by u.activated desc, u.username asc Root u = criteria.from(User.class); criteria.select(u).orderBy( cb.desc(u.get("activated")), cb.asc(u.get("username")) );
Упорядочение атрибутов со значением null Если столбец, по которому производится упорядочение, может содержать NULL, записи с NULL могут оказаться в начале или в конце результатов. Это поведение определяется СУБД, поэтому для создания переносимого приложения всегда нужно указывать, должны ли записи с NULL находиться в конце или начале, добавив предложение ORDER BY ... NULLS FIRST|LAST. Hibernate поддерживает это предложение в JPQL, однако в JPA оно не стандартизовано. Вместо этого можно задать порядок по умолчанию, установив параметр конфигурации единицы хранения hibernate.order_by.default_null_ordering в значение none (по умолчанию), first или last.
Проекции 439 Особенности Hibernate Спецификация JPA позволяет использовать в предложении ORDER BY только те свойства/пути, которые присутствуют в предложении SELECT. Следующие запросы не переносимы, но зато работают в Hibernate: select i.name from Item i order by i.buyNowPrice asc select i from Item i order by i.seller.username desc
Будьте осторожнее с неявными внутренними соединениями в выражениях, представляющих пути, и предложениях ORDER BY: последний запрос вернет только экземпляры Item с заполненным полем seller. Это может оказаться неожиданным, поскольку тот же запрос, но без предложения ORDER BY, вернет все экземпляры Item (давайте на секунду забудем, что в нашей модели поле seller любого объекта Item всегда заполнено). Более подробное обсуждение внутренних соединений и выражений, описывающих пути, вы найдете в следующей главе. Вы уже знаете, как писать предложения FROM, WHERE и ORDER BY. Вы умеете извлекать нужные сущности, применяя различные выражения для ограничения и упорядочивания результатов. Теперь осталось только определить набор возвращаемых атрибутов с помощью проекции.
15.3. Проекции Говоря простым языком, выборки и ограничения в запросе определяют, из каких таблиц и какие записи будут извлекаться. Проекция же определяет, какие «столбцы» нужно вернуть приложению. В JPQL за проекцию отвечает предложение SELECT.
15.3.1. Проекция сущностей и скалярных значений Рассмотрим, к примеру, следующие запросы: select i, b from Item i, Bid b Root i = criteria.from(Item.class); Root b = criteria.from(Bid.class); criteria.select(cb.tuple(i, b)); /* Удобная альтернатива: criteria.multiselect( criteria.from(Item.class), criteria.from(Bid.class) ); */
440 Языки запросов Как упоминалось ранее, с помощью такого запроса на основе критериев можно выбирать экземпляры разных сущностей, представленных объектами Root, вызывая метод from() несколько раз. Для добавления в проекцию нескольких элементов нужно вызвать метод tuple() объекта CriteriaBuilder или более удобный метод multiselect(). Ниже создается декартово произведение всех экземпляров Item и Bid. Запросы возвращают упорядоченные пары экземпляров сущностей Item и Bid: List result = query.getResultList();
Возвращает коллекцию List с элементами типа Object[]
Set items = new HashSet(); Set bids = new HashSet(); for (Object[] row : result) { assertTrue(row[0] instanceof Item); items.add((Item) row[0]);
}
assertTrue(row[1] instanceof Bid); bids.add((Bid)row[1]);
assertEquals(items.size(), 3); assertEquals(bids.size(), 4); assertEquals(result.size(), 12);
Индекс 0 Индекс 1
Декартово произведение
Запрос вернет коллекцию (List) с элементами типа Object[] . Элемент с индексом 0 будет хранить объект Item , а элемент с индексом 1 – объект Bid . Будучи произведением, результат содержит все возможные комбинации запи сей из таблиц, соответствующих сущностям Item и Bid. Очевидно, что в таком запросе нет практического смысла, но вас не должна удивлять возможность получения коллекции элементов Object[] в качестве результата запроса. Hibernate управляет всеми экземплярами сущностей Item и Bid, которые находятся в хранимом состоянии, в контекст хранения. Также обратите внимание, что два множества HashSet устраняют повторяющиеся экземпляры Item и Bid. В API запросов на основе критериев имеется также возможность получения типизированного списка результатов с помощью интерфейса Tuple. Для этого сначала нужно создать объект типа CriteriaQuery, вызвав метод createTupleQuery(). Затем завершить определение запроса, присвоив классам сущностей псевдонимы: CriteriaQuery criteria = cb.createTupleQuery(); // Или: CriteriaQuery criteria = cb.createQuery(Tuple.class); criteria.multiselect( criteria.from(Item.class).alias("i"), criteria.from(Bid.class).alias("b") );
Задавать псевдонимы необязательно
TypedQuery query = em.createQuery(criteria); List result = query.getResultList();
Проекции 441 Интерфейс Tuple поддерживает несколько способов обращения к результатам: по индексу, по псевдониму или с использованием нетипизированного доступа через метаданные: for (Tuple tuple : result) { Item item = tuple.get(0, Item.class); Bid bid = tuple.get(1, Bid.class); item = tuple.get("i", Item.class); bid = tuple.get("b", Bid.class);
}
По индексу По псевдониму
for (TupleElement element : tuple.getElements()) { Class clazz = element.getJavaType(); String alias = element.getAlias(); Object value = tuple.get(element); }
Через метаданные
Следующая проекция также возвращает коллекцию элементов Object[]: select u.id, u.username, u.homeAddress from User u Root u = criteria.from(User.class); Возвращает коллекцию List элементов Object[] criteria.multiselect( u.get("id"), u.get("username"), u.get("homeAddress") );
Каждый элемент Object[], возвращаемый этим запросом, содержит объект типа Long по индексу 0, объект String по индексу 1 и объект Address по индексу 2. Первые два – скалярные значения; третий – экземпляр встроенного класса. Ни один из этих объектов не является управляемым экземпляром сущности! Следовательно, ни один не находится в хранимом состоянии, в отличие от экземпляров сущностей. Они не пересекают границу транзакции, и, очевидно, изменение их состояния не проверяется автоматически. Мы называем такие объекты временными (transient). Подобный запрос обычно требуется для создания простого отчета, который бы показывал все имена пользователей и их домашние адреса. Вы уже несколько раз сталкивались с выражениями для описания путей к атрибутам: с помощью точечной нотации можно ссылаться на такие поля сущностей, как User#username в виде u.username. Для вложенного поля встраиваемого типа, к примеру, можно указать путь u.homeAddress.city.zipcode. Этот путь ссылается на единственное значение, поскольку не завершается именем поля с отображаемой коллекцией. По сравнению с использованием объектов Object[] или Tuple (особенно для создания отчетов), лучше использовать способ динамического создания экземпляров в проекциях, который будет продемонстрирован далее.
15.3.2. Динамическое создание экземпляров Предположим, что существует отчет, в котором нужно отобразить некоторые данные в виде списка. Допустим, нужно показать все аукционные товары с датами
442 Языки запросов окончания каждого аукциона. При этом не хочется загружать управляемые экземпляры сущности Item, поскольку никакие данные меняться не будут. Сначала нужно создать класс ItemSummary с конструктором, принимающим аргументы Long, String и Date, соответствующие идентификатору товара, его названию и отметке времени окончания аукциона: public class ItemSummary { public ItemSummary(Long itemId, String name, Date auctionEnd) { // ... } }
// ...
Иногда экземпляры таких классов называются объектами передачи данных (Data Transfer Objects, DTO), поскольку их главной целью является передача данных из одной части приложения в другую. Класс ItemSummary не отображается в базу данных, и в нем можно определять любые методы (чтения, записи, вывода значений), необходимые для вашего отчета. Hibernate может сразу вернуть из запроса новые экземпляры ItemSummary с помощью оператора new в JPQL и метода construct() в критериях: select new org.jpwh.model.querying.ItemSummary( i.id, i.name, i.auctionEnd ) from Item i Root i = criteria.from(Item.class); criteria.select( cb.construct( Должен быть подходящий конструктор ItemSummary.class, i.get("id"), i.get("name"), i.get("auctionEnd") ) );
Каждый элемент в списке результатов этого запроса будет экземпляром ItemSummary. Обратите внимание, что в JPQL нужно указывать полное квалифицированное имя класса, т. е. добавлять имя пакета. Также отметьте, что использование вложенных конструкторов не поддерживается – нельзя написать new ItemSummary(..., new UserSummary(...)). Динамическое создание экземпляров работает не только с неуправляемыми объектами передачи данных вроде экземпляров ItemSummary. Можно создать новый объект Item или User, уже являющийся экземпляром отображаемого класса сущности. Самое главное, чтобы класс имел подходящий конструктор для использования в проекции. Но при динамическом создании экземпляров сущностей в запросе они не будут находиться в хранимом состоянии! Они будут возвращаться либо во временном, либо в отсоединенном состоянии, в зависимости от значения идентификатора. Как вариант этот прием можно использовать для копирования данных: извлечь «новый» временный экземпляр Item, в конструктор которого
Проекции 443 передается часть значений из базы данных, в часть из приложения, чтобы затем сохранить его в базу, вызвав метод persist(). Если класс объекта DTO не имеет подходящего конструктора, но вы хотите заполнить свойства объекта значениями из результата запроса, используйте ResultTransformer, как показано в разделе 16.1.3. Другие примеры группировки и агрегирования вы увидите далее. А пока рассмотрим одну особенность проекций, которая сбивает с толку многих разработчиков, – обработку повторяющихся записей.
15.3.3. Извлечение уникальных результатов Проекция в запросе не гарантирует уникальности элементов в наборе результатов. Например, имена товаров могут быть неуникальными, поэтому следующий запрос может вернуть одно имя несколько раз: select i.name from Item i CriteriaQuery criteria = cb.createQuery(String.class); criteria.select( criteria.from(Item.class).get("name") );
Трудно представить, какую пользу могли бы принести две одинаковые записи в результате, поэтому если есть вероятность появления одинаковых записей, для их фильтрации обычно используется ключевое слово DISTINCT или метод distinct(): select distinct i.name from Item i CriteriaQuery criteria = cb.createQuery(String.class); criteria.select( criteria.from(Item.class).get("name") ); criteria.distinct(true);
Они удалят повторяющиеся значения из возвращаемого списка имен экземпляров Item и будут преобразованы в SQL-оператор DISTINCT. Фильтрация осуществ ляется на уровне базы данных. Далее вы увидите, что так происходит не всегда. Вы уже видели, как вызывать функции в ограничениях, в предложении WHERE. Аналогично можно вызывать функции в проекциях для изменения возвращаемых результатов.
15.3.4. Вызов функций в проекциях Следующие запросы возвращают строки (String), полученные путем вызова функции concat() внутри проекции: select concat(concat(i.name, ': '), i.auctionEnd) from Item i Root i = criteria.from(Item.class); criteria.select(
444 Языки запросов
);
cb.concat( cb.concat(i.get("name"), ":"), i.get("auctionEnd") Обратите внимание на приведение типа Date к строке )
Запрос вернет список (List) строк (String), каждая из которых будет иметь вид «[имя товара]:[дата окончания аукциона]». Из примера видно, что можно также описывать вложенные вызовы функций. Функция coalesce() возвращает null, если все ее аргументы равны null; в противном случае она вернет первый непустой аргумент: select i.name, coalesce(i.buyNowPrice, 0) from Item i Root i = criteria.from(Item.class); criteria.multiselect( i.get("name"), cb.coalesce(i.get("buyNowPrice"), 0) );
Если товар (Item) не имеет текущей цены покупки (buyNowPrice), вместо null будет возвращено нулевое значение типа BigDecimal. Похожим образом действует выражение case/when, но его возможности гораздо шире. Следующий запрос вернет имя (username) каждого пользователя (User), а также строку (String) с текстом «Германия», «Швейцария» или «Другое» в зависимости от длины индекса (zipcode) пользователя: select u.username, case when length(u.homeAddress.zipcode) = 5 then 'Germany' when length(u.homeAddress.zipcode) = 4 then 'Switzerland' else 'Other' end from User u // Проверьте поддержку строковых литералов; см. описание ошибки в Hibernate // под номером HHH-8124 Root u = criteria.from(User.class); criteria.multiselect( u.get("username"), cb.selectCase() .when( cb.equal( cb.length(u.get("homeAddress").get("zipcode")), 5 ), "Germany" ) .when( cb.equal( cb.length(u.get("homeAddress").get("zipcode")), 4 ), "Switzerland"
Проекции 445
);
) .otherwise("Other")
За информацией о стандартных встроенных функциях обращайтесь к таблицам в предыдущем разделе. В отличие от вызовов функций в ограничениях, встретив неизвестную функцию в проекции, Hibernate не будет отправлять ее в базу данных в виде простого вызова SQL-функции. Любая функция, которая вызывается в проекции, должна быть известна Hibernate и вызываться с помощью специального JPQL-оператора function(). Следующая проекция вернет имя каждого аукционного товара (Item) и количество дней между датой его создания и датой окончания аукциона с помощью SQL-функции datediff() базы данных H2: select i.name, function('DATEDIFF', 'DAY', i.createdOn, i.auctionEnd) from Item i Root i = criteria.from(Item.class); criteria.multiselect( i.get("name"), cb.function( "DATEDIFF", Integer.class, cb.literal("DAY"), i.get("createdOn"), i.get("auctionEnd") ) );
Если, напротив, потребуется вызвать функцию посредственно, сначала нужно сообщить Hibernate тип ее возвращаемого значения, чтобы он мог корректно обработать запрос. Функции для использования в проекциях должны определяться в экземпляре диалекта базы данных org.hibernate.Dialect. Функция datediff(), к примеру, уже определена в диалекте базы данных H2. После этого функцию можно вызвать либо с помощью оператора function(), который будет работать с любыми реализациями JPA при обращении к базе H2, либо напрямую, как datediff(), что, скорее всего, будет работать только с Hibernate. Загляните в исходный код диалекта вашей базы данных – скорее всего, вы найдете множество других зарегистрированных там нестандартных функций SQL. Кроме того, функции SQL можно добавлять программно, при загрузке Hibernate, вызывая метод applySqlFunction() интерфейса MetadataBuilder. В следующем примере показано, как добавить SQL-функцию lpad() во время загрузки Hibernate: ... MetadataBuilder metadataBuilder = metadataSources.getMetadataBuilder(); metadataBuilder.applySqlFunction(
446 Языки запросов
);
"lpad", new org.hibernate.dialect.function.StandardSQLFunction( "lpad", org.hibernate.type.StringType.INSTANCE )
За более подробной информацией обращайтесь к документации Javadoc с описанием SQLFunction и его подклассов. Далее мы рассмотрим агрегирующие функции, самые востребованные при создании отчетов.
15.3.5. Агрегирующие функции Запросы, созданные для получения отчетов, часто используют встроенные возможности баз данных по группировке и агрегации данных. Например, в отчете можно показать самую большую начальную стоимость товара в каждой категории. Эти вычисления будут выполнены на уровне базы данных, и вам не нужно будет загружать много экземпляров сущностей Item в память. В JPA стандартизованы следующие функции агрегирования: count(), min(), max(), sum() и avg(). Следующий запрос подсчитает количество экземпляров Item: select count(i) from Item i criteria.select( cb.count(criteria.from(Item.class)) );
Он вернет результат в виде объекта Long: Long count = (Long)query.getSingleResult();
Функция count(distinct) в JPQL и метод countDistinct() удаляют дубликаты: select count(distinct i.name) from Item i criteria.select( cb.countDistinct( criteria.from(Item.class).get("name") ) );
Следующий запрос вычислит сумму всех ставок (Bid): select sum(b.amount) from Bid b CriteriaQuery criteria = cb.createQuery(Number.class); criteria.select( cb.sum( criteria.from(Bid.class).get("amount") ) );
Проекции 447 Запрос вернет значение типа BigDecimal, поскольку свойство amount имеет тип BigDecimal. Функция sum() также распознает тип BigInteger, а для всех остальных числовых типов возвращает значение типа Long. Следующий запрос вернет минимальную и максимальную ставки для конкретного экземпляра Item: select min(b.amount), max(b.amount) from Bid b where b.item.id = :itemId Root b = criteria.from(Bid.class); criteria.multiselect( cb.min(b.get("amount")), cb.max(b.get("amount")) ); criteria.where( cb.equal( b.get("item").get("id"), cb.parameter(Long.class, "itemId") ) );
Результатом этого запроса будет кортеж объектов BigDecimal (два экземпляра BigDecimal в массиве типа Object[]). Когда агрегирующая функция в предложении SELECT используется без группировки в предложении GROUP BY, в результате будет возвращена единственная запись с агрегированным значением. Это значит, что в отсутствие предложения GROUP BY любое предложение SELECT с агрегирующими функциями должно состоять только из них. Для получения более сложных данных для отчетов нужно выполнить группировку данных.
15.3.6. Группировка данных В JPA определяется поддержка нескольких особенностей SQL, часто используе мых при создании отчетов (хотя они могут применяться и в других случаях). В запросах для создания отчетов сначала записывается предложение SELECT, определяющее проекцию, а затем предложения GROUP BY и HAVING, описыващие агрегирование. Так же как в SQL, любое свойство или псевдоним, стоящие в предложении SELECT отдельно от агрегирующих функций, должны также присутствовать в предложении GROUP BY. Рассмотрим запрос, который подсчитывает количество пользователей с одинаковой фамилией: select u.lastname, count(u) from User u group by u.lastname Root u = criteria.from(User.class); criteria.multiselect(
448 Языки запросов u.get("lastname"), cb.count(u)
); criteria.groupBy(u.get("lastname"));
В данном примере поле u.lastname находится вне агрегирующей функции, поэтому данные в проекции должны быть сгруппированы по u.lastname. Также не нужно указывать свойство, которое должно подсчитываться, поскольку выражение count(u) будет автоматически преобразовано в count(u.id). Следующий запрос вычислит средний размер ставки (Bid#amount) для каждого товара Item: select i.name, avg(b.amount) from Bid b join b.item i group by i.name Root b = criteria.from(Bid.class); criteria.multiselect( b.get("item").get("name"), cb.avg(b.get("amount")) ); criteria.groupBy(b.get("item").get("name"));
Особенности Hibernate При использовании группировок можно столкнуться с некоторыми ограниче ниями в Hibernate. Следующий запрос полностью соответствует спецификации, но Hibernate обрабатывает его некорректно: select i, avg(b.amount) from Bid b join b.item i group by i
Спецификация JPA разрешает указывать в группировке псевдонимы сущностей: group by i. Но Hibernate не подставит свойства сущности Item в сгенерированное предложение SQL GROUP BY, что вызовет несоответствие с предложением SELECT. Все свойства вам придется указывать вручную, пока эта ошибка не будет исправлена в Hibernate (это одна из самых старых ошибок под номером HHH-1615): select i, avg(b.amount) from Bid b join b.item i group by i.id, i.name, i.createdOn, i.auctionEnd, i.auctionType, i.approved, i.buyNowPrice, i.seller Root b = criteria.from(Bid.class); Join i = b.join("item"); criteria.multiselect( i, cb.avg(b.get("amount"))
Соединения 449 ); criteria.groupBy( i.get("id"), i.get("name"), i.get("createdOn"), i.get("auctionEnd"), i.get("auctionType"), i.get("approved"), i.get("buyNowPrice"), i.get("seller") );
Иногда требуется исключить некоторые группировки, выбирая только конкретные агрегированные значения. Ограничение для записей описывается в предложении WHERE. А предложение HAVING описывает ограничение для группировок. Например, следующий запрос подсчитает количество пользователей с фамилией, начинающейся на букву «D»: select u.lastname, count(u) from User u group by u.lastname having u.lastname like 'D%' Root u = criteria.from(User.class); criteria.multiselect( u.get("lastname"), cb.count(u) ); criteria.groupBy(u.get("lastname")); criteria.having(cb.like(u.get("lastname"), "D%"));
Предложения SELECT и HAVING подчиняются общему правилу: вне агрегирующих функций могут находиться только свойства, по которым осуществляется группировка. В предыдущих разделах вы познакомились с основами запросов. Пришло время изучить более продвинутые возможности. У большинства инженеров наибольшие затруднения вызывает соединение произвольных данных с помощью оператора join – мощного механизма реляционной модели.
15.4. Соединения Оператор join соединяет данные из двух (или более) отношений. Соединение данных позволяет извлечь несколько связанных экземпляров и коллекций в одном запросе: например, загрузить экземпляр Item и его коллекцию bids за одно обращение к базе данных. Сейчас мы продемонстрируем, как работают основные операции соединения и как они используются для определения стратегий динамического извлечения. Рассмотрим сначала, как соединения работают в обычном SQL, оставив на минуту JPA.
15.4.1. Соединения в SQL Рассмотрим упомянутый выше пример: соединение таблиц ITEM (товары) и BID (ставки), показанных на рис. 15.1. В базе данных находятся три товара: для первого
450 Языки запросов имеются три ставки, для второго – одна, а для третьего нет ни одной. Обратите внимание, что показаны только некоторые столбцы, остальные заменены многоточиями.
Рис. 15.1 Таблицы ITEM и BID – первые кандидаты для соединения
Большинство думает об операции join в контексте баз данных SQL как о внут реннем соединении (inner join). Внутреннее соединение является наиболее важным типом соединений и самым простым для понимания. Рассмотрим выражение SQL и его результат (рис. 15.2). Это выражение SQL содержит в предложении FROM оператор inner join в стиле ANSI. select i.*, b.* from ITEM i inner join BID b on i.ID = b.ITEM_ID i.ID
i.NAME
...
b.ID
b.ITEM_ID
1
Foo
...
1
1
99.00
1
Foo
...
2
1
100.00
1
Foo
...
3
1
101.00
2
Bar
...
4
2
4.99
b.AMOUNT
Рис. 15.2 Результат внутреннего соединения двух таблиц в стиле ANSI
Выполняя внутреннее соединение таблиц ITEM и BID по условию равенства атрибута ID из таблицы ITEM атрибуту ITEM_ID из таблицы BID, вы получите все товары с их ставками. Обратите внимание, что в результат этого запроса попадут только товары, имеющие ставки. Операцию соединения можно представить следующим образом: сначала берется произведение двух таблиц, т. е. все возможные комбинации записей из ITEM и BID. Затем объединенные записи фильтруются с использованием условия соединения: выражения в предложении ON (любой хороший движок базы данных использует более сложный алгоритм для создания соединения; он обычно не создает затратного произведения, чтобы затем его отфильтровать). Условие соединения – это логическое выражение, возвращающее true, если объединенная запись должна попасть в результат запроса.
Соединения 451 Важно понимать, что условием соединения может быть любое выражение, возвращающее true. Данные можно соединять по-разному; вы не ограничены только сравнением значений идентификаторов. Например, условие соединения on i.ID = b.ITEM_ID and b.AMOUNT > 100 будет истинным только для записей в BID со значением атрибута AMOUNT больше 100. На столбец ITEM_ID таблицы BID наложено ограничение внешнего ключа, гарантирующее присутствие в записи из BID ссылки на запись в ITEM. Но это не означает, что соединение можно выполнять, используя только столбцы первичного и внешнего ключей. Ключевые столбцы, безусловно, чаще других выступают в роли операндов в условиях соединений, поскольку часто требуется извлекать связанную информацию. Чтобы извлечь все товары, а не только со ставками, нужно использовать левое внешнее соединение ((left) outer join), как показано на рис. 15.3. select i.*, b.* from ITEM i left outer join BID b on i.ID = b.ITEM_ID i.ID
i.NAME
...
b.ID
b.ITEM_ID
b.AMOUNT
1
Foo
...
1
1
99.00
1
Foo
...
2
1
100.00
1
Foo
...
3
1
101.00
2
Bar
...
4
2
4.99
3
Baz
...
Рис. 15.3 Результат левого внешнего соединения двух таблиц в стиле ANSI
В случае левого внешнего соединения каждая запись в (левой) таблице ITEM, не удовлетворяющая условию соединения, также будет добавлена в результат, а все столбцы таблицы BID будут содержать для нее значения NULL. Правые внешние соединения применяются редко; разработчики обычно думают слева направо и в операции соединения помещают основную таблицу вначале. На рис. 15.4 показан такой же результат, но только с применением правого внешнего соединения, где ведущей выступает таблица BID, а не ITEM. В SQL условия соединения обычно указываются явно. К сожалению, в качестве условия нельзя использовать имя ограничения внешнего ключа: select * from ITEM join BID on FK_BID_ITEM_ID не сработает. Условие соединения должно быть указано либо в предложении ON, в соответствии с синтаксисом ANSI, либо в предложении WHERE, при использовании так называемого тета-соединения: select * from ITEM i, BID b where i.ID = b.ITEM_ID. Это пример внутреннего соединения; как видите, в предложении FROM сначала создается произведение таблиц. Теперь пришло время изучить возможности JPA. Не забывайте, что в конечном итоге Hibernate превращает все запросы в их SQL-эквиваленты, поэтому даже
452 Языки запросов если синтаксис будет слегка отличаться, вы всегда сможете вернуться к примерам из этого раздела, чтобы проверить свое понимание итогового кода SQL и результатов запросов. select b.*, i.* from BID b right outer join ITEM i on b.ITEM_ID = i.ID b.ID
b.ITEM_ID
b.AMOUNT
i.ID
i.NAME
...
1
1
99.00
1
Foo
...
2
1
100.00
1
Foo
...
3
1
101.00
1
Foo
...
4
2
4.99
2
Bar
...
Baz
...
3
Рис. 15.4 Результат правого внешнего соединения двух таблиц в стиле ANSI
15.4.2. Соединение таблиц в JPA JPA поддерживает четыре способа описания соединений (внешних и внутренних) в запросе: неявное соединение по связи с использованием выражений для представления путей к атрибутам; обычное соединение в предложении FROM с помощью оператора join; немедленное соединение в предложении FROM с использованием оператора join и ключевого слова fetch для немедленного извлечения; тета-соединение в предложении WHERE. Рассмотрим сначала неявные соединения по связи.
15.4.3. Неявные соединения по связи В запросах JPA необязательно явно указывать условия соединения. Достаточно указать имя связи, отображаемой в классе Java. Как этого недостает в SQL – возможности описывать условие соединения с помощью ограничения внешнего ключа. Поскольку большинство отношений по внешнему ключу описывается в схеме базы данных, имена этих отображаемых связей можно использовать в языке запросов. Пусть это всего лишь синтаксический сахар, но он очень удобен. Например, класс сущности Bid имеет отображаемую связь многие к одному с классом Item (с именем item). Если указать эту связь в запросе, Hibernate получит достаточно информации, чтобы сформировать условие соединения со сравнением ключевых столбцов. Благодаря этому запросы становятся короче и понятнее. Ранее в этой главе вы уже сталкивались с выражениями, описывающими пути к атрибутам с помощью точечной нотации: выражения, ссылающиеся на единственное значение, такие как user.homeAddress.zipcode, и выражения, ссылающие-
Соединения 453 ся на коллекцию, такие как item.bids. Такие выражения можно использовать в запросах с неявными соединениями: select b from Bid b where b.item.name like 'Fo%' Root b = criteria.from(Bid.class); criteria.select(b).where( cb.like( b.get("item").get("name"), "Fo%" ) );
Путь b.item.name создаст неявное соединение по связи многие к одному от сущности Bid к сущности Item; эта связь называется item. Hibernate знает, что вы отобразили эту связь с помощью внешнего ключа ITEM_ID в таблице BID, и сможет правильно сформировать условие соединения в SQL. Неявные соединения всегда работают для ассоциаций многие к одному и один ко многим, но не для ассоциаций с коллекциями (нельзя написать item.bids.amount). Одному выражению пути может соответствовать несколько соединений: select b from Bid b where b.item.seller.username = 'johndoe' Root b = criteria.from(Bid.class); criteria.select(b).where( cb.equal( b.get("item").get("seller").get("username"), "johndoe" ) );
Этот запрос соединяет таблицы BID, ITEM и USER. Мы не советуем применять этот стиль в более сложных запросах. Соединения играют важную роль в SQL, поэтому во время оптимизации запросов очень важно иметь возможность с ходу определять количество соединений. Рассмотрим сле дующий запрос: select b from Bid b where b.item.seller.username = 'johndoe' and b.item.buyNowPrice is not null Root b = criteria.from(Bid.class); criteria.select(b).where( cb.and( cb.equal( b.get("item").get("seller").get("username"), "johndoe" ), cb.isNotNull(b.get("item").get("buyNowPrice")) ) );
454 Языки запросов Сколько соединений потребуется, чтобы выразить этот запрос на языке SQL? Даже если вы правильно ответите на вопрос, это отнимет у вас несколько секунд. Правильный ответ – два. Сформированный код SQL будет выглядеть примерно так: select b.* from BID b inner join ITEM i on b.ITEM_ID = i.ID inner join USER u on i.SELLER_ID = u.ID where u.USERNAME = 'johndoe' and i.BUYNOWPRICE is not null;
Альтернативой соединениям с такими сложными выражениями для описания путей служат обычные соединения с предложениями FROM.
15.4.4. Явные соединения JPA различает цели соединений. Предположим, что вы выбираете все товары; соединение с таблицей ставок может преследовать две цели. Вы можете ограничить количество товаров, возвращаемое запросом, на основе какого-либо критерия, применяемого к ставкам. Например, можно получить все товары со ставками больше 100, что потребует внутреннего соединения. В этом случае вас не будут интересовать товары без ставок. С другой стороны, главной целью может быть получение всех товаров, а ставки присоединяются только затем, чтобы получить все данные в одном выражении SQL, что мы ранее назвали немедленным извлечением через соединение. Помните, что по умолчанию мы отображаем все связи как отложенные, поэтому немедленное извлечение переопределит стратегию извлечения по умолчанию в конкретном запросе во время выполнения. Давайте сначала напишем несколько запросов, использовав соединения для ограничения результатов. Чтобы извлечь все экземпляры Item, оставив только со ставками, превышающими определенное значение, нужно присвоить псевдоним связи соединения. Затем на него можно будет сослаться в предложении WHERE для описания ограничения: select i from Item i join i.bids b where b.amount > 100 Root i = criteria.from(Item.class); Join b = i.join("bids"); criteria.select(i).where( cb.gt(b.get("amount"), new BigDecimal(100)) );
В этом запросе коллекции bids присвоен псевдоним b, с помощью которого описывается ограничение для экземпляров Item, чтобы извлечь только те, которые имеют значение Bid#amount больше 100. До сих пор в этом разделе вы видели только внутренние соединения. Внешние соединения в основном применяются для динамического извлечения, которое мы
Соединения 455 скоро обсудим. Но иногда бывает нужно написать простой запрос с внешним соединением без применения динамической стратегии извлечения. Следующий запрос, к примеру, извлечет товары без ставок и со ставками больше минимального значения: select i, b from Item i left join i.bids b on b.amount > 100 Root i = criteria.from(Item.class); Join b = i.join("bids", JoinType.LEFT); b.on( cb.gt(b.get("amount"), new BigDecimal(100)) ); criteria.multiselect(i, b);
Запрос вернет кортеж объектов Item и Bid в виде коллекции List. Первое, что бросается в глаза, – ключевое слово LEFT и аргумент JoinType.LEFT в запросе на основе критериев. Вы можете использовать форму LEFT OUTER JOIN и RIGHT OUTER JOIN в запросах JPQL, но мы предпочитаем короткий вариант. Вторая особенность – дополнительное условие соединения после ключевого слова ON. Если поместить в предложение WHERE условие b.amount > 100, в результат запроса попадут только экземпляры Item, имеющие ставки. Но это не то, что нам надо: нам нужно извлечь товары и ставки, а также товары без ставок. Если товар имеет ставку, она должна быть больше 100. Добавляя дополнительное условие соединения в предложение FROM, мы накладываем ограничение на экземпляры Bid, по-прежнему возвращая все экземпляры Item, независимо от наличия ставок. Дополнительное условие соединения будет преобразовано в код SQL: ... from ITEM i left outer join BID b on i.ID = b.ITEM_ID and (b.AMOUNT > 100)
Запрос SQL всегда будет содержать неявное условие соединения отображаемой связи i.ID = b.ITEM_ID. В условие соединения можно добавить лишь дополнительное выражение. JPA и Hibernate не поддерживают произвольных внешних соединений без применения отображаемых связей сущностей или коллекций. В Hibernate есть нестандартное ключевое слово WITH – эквивалент ON в JPQL. Его можно встретить в старом коде, поскольку поддержка ON в JPA была стандартизована лишь недавно. Можно написать запрос, возвращающий те же данные, но с помощью правого внешнего соединения, поменяв ведущую таблицу: select b, i from Bid b right outer join b.item i where b is null or b.amount > 100 Root b = criteria.from(Bid.class); Join i = b.join("item", JoinType.RIGHT); criteria.multiselect(b, i).where(
456 Языки запросов
);
cb.or( cb.isNull(b), cb.gt(b.get("amount"), new BigDecimal(100)))
Правое внешнее соединение гораздо важнее, чем вы могли бы подумать. Ранее в этой книге мы советовали избегать по мере возможности отображения хранимых коллекций. Поэтому если у вас нет коллекции типа один ко многим Item#bids, вам понадобится правое внешнее соединение, чтобы извлечь все экземпляры Item с соответствующими им экземплярами Bid. Запрос начинается с «другой» стороны – отображения многие к одному Bid#item. Отложенные внешние соединения также играют важную роль в немедленном динамическом извлечении.
15.4.5. Динамическое извлечение с помощью соединений Все запросы из предыдущих разделов имели кое-что общее: все возвращаемые экземпляры Item имели коллекцию bids. Когда коллекция, отмеченная аннотацией @OneToMany, отображается с параметром FetchType.LAZY (по умолчанию для коллекций), она будет инициализирована с помощью отдельного выражения SQL при первом обращении к ней. Так же работают все отношения, связанные только с одной сущностью, такие как связь seller с аннотацией @ManyToOne у каждого экземпляра Item. По умолчанию Hibernate создаст прокси-объект и выполнит отложенную загрузку связанного экземпляра User только при первом обращении. Но что делать, если нужно изменить это поведение? Во-первых, можно поменять план извлечения в метаданных и отобразить коллекцию или связь с параметром FetchType.EAGER. После этого Hibernate выполнит весь необходимый код SQL, чтобы обеспечить загрузку требуемого графа объектов. Это, в свою очередь, означает, что один запрос в JPA может привести к выполнению нескольких операций в SQL! Например, простой запрос select i from Item i может привести к выполнению дополнительных выражений SQL для загрузки коллекции bids каждого объекта Item, свойства seller каждого объекта Item и т. д. В главе 12 мы показали пример с глобальным планом отложенного извлечения, который определялся в метаданных отображения, где мы не должны были использовать параметра FetchType.EAGER для коллекций и связей. Но затем, для конкретного варианта использования, динамически переопределили план отложенного извлечения и написали запрос, извлекающий данные самым эффективным способом. Например, не нужно выполнять несколько выражений SQL, чтобы извлечь все экземпляры Item с инициализированными коллекциями bids и установленным значением seller каждого экземпляра Item. Это можно сделать одним выражением SQL с помощью операции соединения. Немедленное извлечение связанных данных осуществляется с помощью ключевого слова FETCH в JPQL и с помощью метода fetch() в API запросов на основе критериев.
Соединения 457 select i from Item i left join fetch i.bids Root i = criteria.from(Item.class); i.fetch("bids", JoinType.LEFT); criteria.select(i);
Вы уже видели код SQL, в который транслируется этот запрос, а также результат запроса на рис. 15.3. Запрос возвращает список List; каждый экземпляр Item вместе со своей коллекцией bids полностью инициализирован. Это отличается от тех кортежей, что вы получали в предыдущем разделе! Но будьте внимательны – возможно, вы не ожидаете получить от предыдущего запроса повторяющихся результатов: List result = query.getResultList(); assertEquals(result.size(), 5);
3 товара, 4 ставки, 5 записей в результате
Set distinctResult = new LinkedHashSet(result); Всего лишь три товара assertEquals(distinctResult.size(), 3); Удаление дубликатов в памяти
Убедитесь, что понимаете, почему повторяющиеся записи появились в результирующей коллекции List. Проверьте еще раз количество «записей» Item в результате, как показано на рис. 15.3. Hibernate превратит записи в элементы списка; но вам может понадобиться точное количество записей для вывода таблицы в отчете. Вы можете избавиться от повторяющихся экземпляров Item, передав получившийся список List в конструктор множества LinkedHashSet, которое удалит дубликаты, но сохранит порядок элементов. Также Hibernate может удалять повторяющиеся элементы с помощью операции DISTINCT и метода distinct() в запросе на основе критериев: select distinct i from Item i left join fetch i.bids Root i = criteria.from(Item.class); i.fetch("bids", JoinType.LEFT); criteria.select(i).distinct(true);
Обратите внимание, что в данном случае операция DISTINCT выполнится не в базе данных. В запросе SQL не будет ключевого слова DISTINCT. Фактически вы не можете удалить повторяющихся записей на уровне объекта ResultSet в SQL. Hibernate удалит их в памяти так же, как вы делали это с помощью LinkedHashSet. Такой же синтаксис может использоваться для немедленного извлечения связей многие к одному и один к одному: select distinct i from Item i left join fetch i.bids b join fetch b.bidder left join fetch i.seller Root i = criteria.from(Item.class);
458 Языки запросов Fetch b = i.fetch("bids", JoinType.LEFT); b.fetch("bidder"); i.fetch("seller", JoinType.LEFT); criteria.select(i).distinct(true);
Столбцы внешних ключей не могут содержать null. Внутреннее соединение или внешнее – в данном случае не важно
Этот запрос вернет список List, и каждый экземпляр Item получит инициализированную коллекцию bids. Свойство seller каждого экземпляра Item также будет загружено. И наконец, будет инициализировано свойство bidder (пользователь, сделавший ставку) каждого экземпляра Bid. Это можно сделать в одном запросе SQL, соединив таблицы ITEM, BID и USERS. Если написать JOIN FETCH без ключевого слова LEFT, будет выполнена отложенная загрузка с внутренним соединением (как если бы вы написали INNER JOIN FETCH). Немедленное внутреннее соединение логично использовать, когда извлекаемые объекты гарантированно существуют: экземпляр Item должен иметь установленное свойство seller, а экземпляр Bid – свойство bidder. Количество одновременно извлекаемых связей при немедленной загрузке, как и число возвращаемых запросом записей, ограничено. Рассмотрим следующий запрос, который инициализирует коллекции Item#bids и Item#images: select distinct i from Item i left join fetch i.bids left join fetch i.images Root i = criteria.from(Item.class); i.fetch("bids", JoinType.LEFT); i.fetch("images", JoinType.LEFT); Декартово произведение – это плохо criteria.select(i).distinct(true);
Этот запрос плох тем, что создает декартово произведение коллекций bids и images и может вернуть результат огромного объема. Мы рассмотрели эту проблему в разделе 12.2.2. Стратегия немедленного динамического извлечения в запросах имеет следующие подводные камни. Никогда не присваивайте псевдонимы любым связям, извлекаемым немедленно, для использования в ограничениях или проекциях. Запрос left join fetch i.bids b where b.amount ... не сработает. Нельзя сказать Hibernate: «Загрузи экземпляры Item и инициализируй коллекцию bids только экземплярами Bid с определенным значением». Вы можете присваивать псевдонимы немедленно извлекаемым связям для дальнейшего уточнения, например для извлечения поля bidder каждого объекта Bid: left join fetch i.bids b join fetch b.bidder. Не извлекайте более одной коллекции; в противном случае вы получите декартово произведение. Но связей с единственным значением можно извлечь любое количество – это не приведет к созданию произведения. Запросы игнорируют любые стратегии извлечения, определяемые в метаданных отображения с помощью аннотации @org.hibernate.annotations.
Соединения 459 Fetch. Например, отображение коллекции bids с параметром org.hibernate. annotations.FetchMode.JOIN никак не повлияет на выполняемые запросы. Динамическая стратегия извлечения игнорирует глобальную стратегию извлечения. С другой стороны, Hibernate никогда не игнорирует отображаемого плана извлечения: Hibernate всегда выбирает параметр FetchType.EAGER, и во время выполнения запроса вы можете увидеть несколько дополнительных выражений SQL. При немедленном извлечении коллекции возвращаемый Hibernate список List содержит столько же записей, сколько вернул запрос SQL, в том числе повторяющиеся ссылки. Вы можете избавиться от дубликатов в памяти – вручную, с помощью LinkedHashSet, или с помощью ключевого слова DISTINCT в запросе. Есть еще один нюанс, заслуживающий внимания. При немедленном извлечении коллекции вы не сможете извлекать результаты запроса из базы данных постранично. Как, например, должен отреагировать запрос select i from Item i fetch i.bids на параметры Query#setFirstResult(21) и Query#setMaxResults(10)? Очевидно, что вы ожидаете извлечь 10 товаров, начиная с 21-го. Но при этом вы хотите немедленно извлечь всю коллекцию bids экземпляра Item. В таком случае база данных не сможет обеспечить постраничного вывода; вы не сможете ограничить результат запроса SQL десятью произвольными записями. Если в запросе коллекция извлекается немедленно, Hibernate выполнит постраничную выборку в памяти приложения. Это означает, что в памяти окажутся все экземпляры Item, и каждый будет иметь инициализированную коллекцию bids. После этого Hibernate вернет требуемую страницу результатов, например только товары с 21 по 30. Но в памяти могут поместиться не все товары, и вы, написав такой запрос, вероятно, ожидали, что постраничная выборка будет выполняться на стороне базы данных! Поэтому Hibernate запишет в журнал предупреждение, если встретит в запросе fetch [collectionPath] и вы вызвали setFirstResult() или setMaxResults(). Мы не рекомендуем использовать fetch [collectionPath] вместе с вызовами setMaxResults() и setFirstResult(). Как правило, всегда можно написать более простой запрос, извлекающий данные, необходимые для отображения, – мы надеемся, что вы не станете использовать постраничную выборку для изменения данных. Если вам, к примеру, потребуется показать несколько страниц с товарами и напротив каждого указать количество сделанных ставок, используйте следующий запрос: select i.id, i.name, count(b) from Item i left join i.bids b group by i.id, i.name
Результат этого запроса база данных сможет вывести постранично после вызова методов setFirstResult() и setMaxResults(). Это будет гораздо эффективнее, чем извлечение любых экземпляров Item или Bid в память; пусть всю работу сделает база данных. Последним способом соединения в JPA является тета-соединение.
460 Языки запросов
15.4.6. Тета-соединения В традиционном SQL тета-соединение – это декартово произведение с условием соединения в предложении WHERE, ограничивающим это произведение. В запросах JPA синтаксис тета-соединения применяется, когда условие соединения не является связью по внешнему ключу, отображаемой на связь класса. Предположим, что вы используете имя пользователя (User) для записи в журнал, а не отображаете связь между LogRecord и User. Этим классам ничего не известно друг о друге, поскольку они не связаны. Вы можете найти все экземпляры User и соответствующие им записи в таблице LogRecord с помощью следующего тета-соединения: select u, log from User u, LogRecord log where u.username = log.username Root u = criteria.from(User.class); Root log = criteria.from(LogRecord.class); criteria.where( cb.equal(u.get("username"), log.get("username"))); criteria.multiselect(u, log);
Условие соединения в данном случае сравнивает атрибуты username обоих классов. Если обе записи будут иметь одинаковое значение username, они попадут в результат. Результат запроса будет состоять из кортежей: List result = query.getResultList(); for (Object[] row : result) { assertTrue(row[0] instanceof User); assertTrue(row[1] instanceof LogRecord); }
Возможно, вы будете прибегать к тета-соединениям не слишком часто. Обратите внимание, что в настоящий момент в JPA нельзя задать внешнее соединение для таблиц, не связанных отображаемой связью; тета-соединения – это внутренние соединения. Другим распространенным случаем применения тета-соединений является сравнение первичного или внешнего ключа с параметрами запроса или с другими внешними ключами в предложении WHERE: select i, b from Item i, Bid b where b.item = i and i.seller = b.bidder Root i = criteria.from(Item.class); Root b = criteria.from(Bid.class); criteria.where( cb.equal(b.get("item"), i), cb.equal(i.get("seller"), b.get("bidder")) ); criteria.multiselect(i, b);
Соединения 461 Этот запрос вернет пары экземпляров Item (товар) и Bid (ставка), где пользователь, сделавший ставку (bidder), является также продавцом (seller). Для CaveatEmptor этот запрос играет важную роль, поскольку позволяет вычислить пользователей, делающих ставки за свой собственный товар. Возможно, этот запрос стоит преобразовать в ограничение базы данных, чтобы не допустить сохранения подобных экземпляров Bid. В предыдущем запросе также присутствует интересное выражение сравнения: i.seller = b.bidder. Это сравнение идентификаторов, которое рассматривается в следующем разделе.
15.4.7. Сравнение идентификаторов JPA поддерживает синтаксис неявного сравнения идентификаторов в запросах: select i, u from Item i, User u where i.seller = u and u.username like 'j%' Root i = criteria.from(Item.class); Root u = criteria.from(User.class); criteria.where( cb.equal(i.get("seller"), u), cb.like(u.get("username"), "j%") ); criteria.multiselect(i, u);
В этом запросе свойство i.seller ссылается на столбец внешнего ключа SELLER_ ID таблицы ITEM, который ссылается на таблицу USERS. Псевдоним u ссылается на первичный ключ таблицы USERS (столбец ID). Следовательно, этот запрос с тетасоединением является более простым эквивалентом следующего: select i, u from Item i, User u where i.seller.id = u.id and u.username like 'j%' Root i = criteria.from(Item.class); Root u = criteria.from(User.class); criteria.where( cb.equal(i.get("seller").get("id"), u.get("id")), cb.like(u.get("username"), "j%") ); criteria.multiselect(i, u);
Особенности Hibernate Выражение пути к атрибуту, оканчивающееся на id, имеет особое значение в Hibernate: имя id всегда ссылается на свойство идентификатора сущности. Не важно, какое имя на самом деле имеет свойство идентификатора, отмеченное аннотацией @Id; к нему всегда можно обратиться как псевдонимСущности.id. Поэтому мы советуем всегда давать свойству идентификатора имя id, чтобы избежать путаницы в за-
462 Языки запросов просах. Обратите внимание, что это не является требованием JPA; особое значение имени id придается только в Hibernate. Также может понадобиться сравнить свойство с параметром запроса, например чтобы найти товары (Item) конкретного продавца (User): select i from Item i where i.seller = :seller Root i = criteria.from(Item.class); criteria.where( cb.equal( i.get("seller"), cb.parameter(User.class, "seller") ) ); criteria.select(i); query.setParameter("seller", someUser); List result = query.getResultList();
Этот запрос также можно выразить в терминах идентификаторов, а не ссылок на объекты. Следующие запросы эквивалентны предыдущим: select i from Item i where i.seller.id = :sellerId Root i = criteria.from(Item.class); criteria.where( cb.equal( i.get("seller").get("id"), cb.parameter(Long.class, "sellerId") ) ); criteria.select(i); query.setParameter("sellerId", USER_ID); List result = query.getResultList();
Что касается идентификаторов, есть существенная разница межу этой парой запросов select b from Bid b where b.item.name like 'Fo%' Root b = criteria.from(Bid.class); criteria.select(b).where( cb.like( b.get("item").get("name"), "Fo%" ) );
и этой: select b from Bid b where b.item.id = :itemId CriteriaQuery criteria = cb.createQuery(Bid.class); Root b = criteria.from(Bid.class);
Подзапросы 463 criteria.where( cb.equal( b.get("item").get("id"), cb.parameter(Long.class, "itemId") ) ); criteria.select(b);
В первой паре используется неявное соединение таблиц; во второй – соединений нет вообще! На этом мы заканчиваем обсуждение запросов с соединениями. Нашей последней темой станут запросы внутри запросов, т. е. подзапросы.
15.5. Подзапросы Подзапросы – это важная и мощная возможность SQL. Подзапрос – это запрос, встроенный в другой запрос, как правило, в предложении SELECT, FROM или WHERE. JPA допускает применение подзапросов только в предложении WHERE. Подзапросы в предложении FROM не поддерживаются, поскольку в языках запросов отсутствует транзитивное замыкание. Результат запроса может быть непригоден для дальнейшей выборки в предложении FROM. Подзапросы в предложении SELECT также не поддерживаются, но можно отображать подзапросы на вычисляемые свойства с помощью аннотации @org.hibernate.annotations.Formula, как показано в разделе 5.1.3. Подзапросы также могут быть коррелированными.
15.5.1. Коррелированные и некореллированные подзапросы Подзапрос может вернуть одну или несколько записей. Как правило, подзапросы, возвращающие одну запись, выполняют агрегацию. Следующий подзапрос вернет количество товаров, проданных пользователем; внешний запрос вернет всех пользователей, продавших более одного товара: select u from User u where ( select count(i) from Item i where i.seller = u ) > 1 Root u = criteria.from(User.class); Subquery sq = criteria.subquery(Long.class); Root i = sq.from(Item.class); sq.select(cb.count(i)) .where(cb.equal(i.get("seller"), u) ); criteria.select(u); criteria.where(cb.greaterThan(sq, 1L));
464 Языки запросов Внутренний запрос является коррелированным – он обращается к псевдониму u из внешнего запроса. В следующем примере используется некоррелированный подзапрос: select b from Bid b where b.amount + 1 >= ( select max(b2.amount) from Bid b2 ) Root b = criteria.from(Bid.class); Subquery sq = criteria.subquery(BigDecimal.class); Root b2 = sq.from(Bid.class); sq.select(cb.max(b2.get("amount"))); criteria.select(b); criteria.where( cb.greaterThanOrEqualTo( cb.sum(b.get("amount"), new BigDecimal(1)), sq ) );
Здесь подзапрос возвращает самую большую ставку во всем приложении; внешний запрос найдет все ставки со значениями, отличающимися от наибольшей не более чем на единицу (долларов, евро и т. д.). Обратите внимание, что в обоих примерах подзапрос в JPQL окружают скобки. Это обязательное требование. Некоррелированные подзапросы безвредны, поэтому используйте их, когда это удобно. Такие запросы всегда можно разделить на два отдельных запроса, поскольку они не ссылаются друг на друга. Но не забывайте оценивать производительность коррелированных подзапросов. В больших базах данных стоимость простого коррелированного подзапроса сравнима со стоимостью соединения. Но переписать коррелированный подзапрос в виде двух отдельных запросов возможно не всегда. В случае, когда подзапрос возвращает несколько записей, к нему применяются кванторы.
15.5.2. Кванторы В стандарте определены следующие кванторы: ALL – выражение вернет true, если результат сравнения будет истинным для всех значений в результатах подзапроса. Если хотя бы для одного значения условие не выполнится, выражение вернет false; ANY – выражение вернет true, если результат сравнения будет истинным хотя бы для одного (любого) значения в результате подзапроса. Если подзапрос не вернет результатов или ни одно значение не удовлетворяет условию сравнения, выражение вернет false. Ключевое слово SOME является синонимом для ANY; EXISTS – выражение вернет true, если подзапрос вернет одно или более значений.
Подзапросы 465 Например, следующий запрос найдет товары со ставками не выше 10: select i from Item i where 10 >= all ( select b.amount from i.bids b ) Root i = criteria.from(Item.class); Subquery sq = criteria.subquery(BigDecimal.class); Root b = sq.from(Bid.class); sq.select(b.get("amount")); sq.where(cb.equal(b.get("item"), i)); criteria.select(i); criteria.where( cb.greaterThanOrEqualTo( cb.literal(new BigDecimal(10)), cb.all(sq) ) );
Следующий запрос вернет товары со ставками, равными 101: select i from Item i where 101.00 = any ( select b.amount from i.bids b ) Root i = criteria.from(Item.class); Subquery sq = criteria.subquery(BigDecimal.class); Root b = sq.from(Bid.class); sq.select(b.get("amount")); sq.where(cb.equal(b.get("item"), i)); criteria.select(i); criteria.where( cb.equal( cb.literal(new BigDecimal("101.00")), cb.any(sq) ) );
Чтобы найти все товары со ставками, примените к результату подзапроса квантор EXISTS: select i from Item i where exists ( select b from Bid b where b.item = i ) Root i = criteria.from(Item.class); Subquery sq = criteria.subquery(Bid.class);
466 Языки запросов Root b = sq.from(Bid.class); sq.select(b).where(cb.equal(b.get("item"), i)); criteria.select(i); criteria.where(cb.exists(sq));
Этот запрос гораздо важнее, чем кажется. Найти все товары со ставками можно также с помощью запроса: select i from Item i where i.bids is not empty. Но это требует наличия отображаемой коллекции типа один ко многим Item#bids. Если вы следуете нашим рекомендациям, вы, скорее всего, отобразили «обратную» сторону отношения: связь типа многие к одному Bid#item. Но тот же результат можно получить с помощью квантора exists() и подзапроса. Подзапросы – это продвинутая технология; вас всегда должно настораживать большое количество подзапросов, поскольку запросы с ними часто можно переписать с использованием соединений и функций. Но иногда и они могут быть полезными.
15.6. Резюме Если до прочтения этой главы вы уже были знакомы с SQL, теперь вы сможете писать различные запросы с помощью JPQL или API запросов на основе критериев. Если вы чувствуете себя неуверенно с SQL, обращайтесь к справочному разделу. С помощью выборки описывается источник(и) данных – «таблицы», для которых пишется запрос. Затем применяются критерии ограничения, чтобы получить из источника требуемое подмножество «записей». Проекция определяет, какие «столбцы» будут возвращены запросом. Также есть возможность эффективно агрегировать данные на уровне базы данных. Мы рассмотрели соединения: как выбирать, ограничивать и объединять данные из нескольких таблиц. Приложению, использующему JPA, соединения нужны для немедленной загрузки экземпляров сущностей и коллекций за одно обращение к базе данных. Это особенно важно, когда требуется уменьшить нагрузку на базу данных, и мы советуем вам еще раз просмотреть все примеры, чтобы точно понимать работу соединений и стратегии немедленного извлечения данных. Запросы можно вкладывать в другие запросы, создавая подзапросы.
Глава
16 Дополнительные возможности запросов
В этой главе: преобразование результатов запросов; фильтрация коллекций; создание запросов на основе критериев с помощью Hibernate.
В этой главе рассматриваются дополнительные возможности запросов: преобразование результатов, фильтрация коллекций и средства в Hibernate для создания запросов на основе критериев. Сначала рассмотрим интерфейс ResultTransformer, который позволяет применять преобразования, отличные от используемых в Hibernate по умолчанию. В предыдущих главах мы советовали быть осторожнее при отображении коллекций, поскольку это овчинка редко стоит выделки. В этой главе мы познакомим вас с фильтрами для коллекций – оригинальной функциональностью Hibernate, позволяющей более эффективно использовать хранимые коллекции. Наконец, мы познакомим вас с нестандартным интерфейсом org.hibernate.Criteria, а также рассмотрим ситуации, когда его лучше использовать вместо стандартных запросов на основе критериев JPA. Начнем с преобразования результатов запросов. Особенности Hibernate
16.1. Преобразование результатов запросов С помощью специального преобразователя можно отфильтровать результат запроса или обработать его с помощью своей процедуры. В Hibernate определено несколько преобразователей, которые можно замещать или настраивать. Мы предполагаем преобразовать результат обычного запроса, но для этого нам понадобится получить доступ к оригинальному интерфейсу org.hibernate.Query с помощью экземпляра Session, как показано в листинге 16.1.
468 Дополнительные возможности запросов Листинг 16.1 Простой запрос с проекцией нескольких атрибутов Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ TransformResults.java Session session = em.unwrap(Session.class); org.hibernate.Query query = session.createQuery( "select i.id as itemId, i.name as name, i.auctionEnd as auctionEnd from Item i" );
Без каких-либо преобразований этот запрос вернет список List с элементами типа Object[]: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ TransformResults.java List result = query.list(); for (Object[] tuple : result) { Long itemId = (Long) tuple[0]; String name = (String) tuple[1]; Date auctionEnd = (Date) tuple[2]; // ... }
Каждый массив объектов – это «запись» из результатов запроса. К элементу кортежа можно обратиться по индексу: 0 – соответствует атрибуту типа Long, 1 String, а 2 – Date. Первый преобразователь, который мы рассмотрим, позволяет получить список List с элементами типа List. Преобразование результатов запроса на основе критериев Все примеры в этом разделе написаны для запросов JPQL, созданных с помощью org.hibernate.Query. Если создать объект запроса JPA типа CriteriaQuery с по мощью интерфейса CriteriaBuilder, вы не сможете применить к нему преобразователь org.hibernate.transform.ResultTransformer: этот интерфейс доступен только в Hibernate. Даже если вы получите оригинальный Hibernate API для своего запроса на основе критериев (путем приведения к типу HibernateQuery, как показано в разделе 14.1.3), вы все равно не сможете применить произвольного преобразователя. К объектам запросов JPA типа CriteriaQuery Hibernate применяет встроенный преобразователь, соответствующий спецификации JPA; применение произвольного преобразователя переопределит это поведение и создаст проблемы. Однако для JPQL-запросов, созданных с помощью javax.persistence.Query, можно установить свой преобразователь, получив доступ к оригинальному интерфейсу HibernateQuery. Кроме того, далее вы увидите оригинальный интерфейс org.hibernate.Criteria – альтернативный механизм для запросов на основе критериев, поддерживающий возможность переопределения org.hibernate.transform.ResultTransformer.
Преобразование результатов запросов 469
16.1.1. Получение списка списков Предположим, что вы хотите получить доступ по индексу, но вам не нравится тип Object[]. Вместо списка элементов Object[] каждый кортеж можно представить как список List, задействовав преобразователь ToListResultTransformer: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ TransformResults.java query.setResultTransformer( ToListResultTransformer.INSTANCE ); List result = query.list(); for (List list : result) { Long itemId = (Long) list.get(0); String name = (String) list.get(1); Date auctionEnd = (Date) list.get(2); // ... }
Отличие совсем незначительное, но это может быть удобно, если другие уровни приложения уже работают со списками списков. Следующий преобразователь представляет каждый кортеж в виде словаря Map, отображающего псевдонимы в соответствующие элементы проекции.
16.1.2. Получение списка словарей Преобразователь AliasToEntityMapResultTransformer возвращает список элементов типа java.util.Map: по одному на каждую «запись». Псевдонимами в запросе являются itemId, name и auctionEnd: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ TransformResults.java query.setResultTransformer( AliasToEntityMapResultTransformer.INSTANCE ); List result = query.list(); Псевдонимы, используемые в запросе assertEquals( query.getReturnAliases(), new String[]{"itemId", "name", "auctionEnd"} );
for (Map map : result) { Long itemId = (Long) map.get("itemId"); String name = (String) map.get("name"); Date auctionEnd = (Date) map.get("auctionEnd"); // ... }
470 Дополнительные возможности запросов Если псевдонимы в запросе неизвестны, их можно получить динамически, вызвав метод org.hibernate.Query#getReturnAliases(). В нашем примере запрос возвращает скалярные значения, но вам также может понадобиться преобразовывать результаты, содержащие экземпляры сущностей. Следующий пример использует псевдонимы для сущностей из проекции и список List элементов типа Map: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ TransformResults.java org.hibernate.Query entityQuery = session.createQuery( "select i as item, u as seller from Item i join i.seller u" ); entityQuery.setResultTransformer( AliasToEntityMapResultTransformer.INSTANCE ); List result = entityQuery.list(); for (Map map : result) { Item item = (Item) map.get("item"); User seller = (User) map.get("seller");
}
assertEquals(item.getSeller(), seller); // ...
Еще большую пользу может принести следующий преобразователь, отобра жающий атрибуты результатов запроса в свойства компонента JavaBean по их псевдонимам.
16.1.3. Отображение атрибутов в свойства компонента JavaBean В разделе 15.3.2 мы показали, как динамически получить объекты JavaBean, используя конструктор ItemSummary. В JPQL это можно сделать с помощью оператора new. В запросах на основе критериев – с помощью метода construct(). Класс ItemSummary должен иметь конструктор, соответствующий набору элементов в проекции. Но даже если в классе компонента JavaBean отсутствует нужный конструктор, все равно можно создать его экземпляр и заполнить его с помощью методов записи и/или прямым обращением к полям с применением объекта AliasToBeanResultTransformer. Следующий пример преобразует результаты запроса, показанного в листинге 16.1: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ TransformResults.java query.setResultTransformer( new AliasToBeanResultTransformer(ItemSummary.class) );
Преобразование результатов запросов 471 List result = query.list(); for (ItemSummary itemSummary : result) { Long itemId = itemSummary.getItemId(); String name = itemSummary.getName(); Date auctionEnd = itemSummary.getAuctionEnd(); // ... }
Конструктору преобразователя нужно передать класс компонента JavaBean; здесь это класс ItemSummary. Hibernate требует, чтобы такой класс не имел конструктора или имел общедоступный конструктор без параметров. Во время преобразования результатов запроса Hibernate отыскивает методы записи и поля, имена которых совпадают с псевдонимами в запросе. В классе ItemSummary должны быть определены поля itemId, name и auctionEnd или методы setItemId(), setName() и setAuctionEnd(). Поля и методы должны иметь правильный тип. Если только часть псевдонимов из запроса отображается на поля класса, а оставшаяся часть – на методы записи, это тоже нормально. Вам также будет полезно узнать, как написать собственный преобразователь ResultTransformer на тот случай, если ни один из существующих не подходит.
16.1.4. Создание преобразователя ResultTransformer Преобразователи, встроенные в Hibernate, довольно простые; между результатами, представленными в виде списков, словарей или массивов объектов, нет особой разницы. Несмотря на то что реализация интерфейса ResultTransformer тривиальна, дополнительная логика преобразования результатов запроса может усилить связанность уровней кода приложения. Если код пользовательского интерфейса уже знает, как отображать таблицу на основе списка List, пусть Hibernate возвращает его напрямую из запроса. Далее мы покажем, как реализовать преобразователь ResultTransformer. Предположим, что требуется получить список List из запроса в листинге 16.1, но так, чтобы Hibernate не участвовал в создании экземпляров ItemSummary, вызывая конструктор через механизм рефлексии. Возможно, класс ItemSummary предопределен и не имеет нужного конструктора, полей или методов. Зато у вас есть фабрика ItemSummaryFactory, производящая экземпляры ItemSummary. Интерфейс ResultTransformer требует реализации методов transformTuple() и transformList(): Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ TransformResults.java query.setResultTransformer( new ResultTransformer() { Преобразует записи в результате @Override public Object transformTuple(Object[] tuple, String[] aliases) {
Long itemId = (Long) tuple[0];
472 Дополнительные возможности запросов String name = (String) tuple[1]; Date auctionEnd = (Date) tuple[2]; При необходимости можно получить assertEquals(aliases[0], "itemId"); assertEquals(aliases[1], "name"); все псевдонимы запроса assertEquals(aliases[2], "auctionEnd");
}
);
}
return ItemSummaryFactory.newItemSummary( itemId, name, auctionEnd );
Преобразование списка результатов @Override public List transformList(List collection) { return Collections.unmodifiableList(collection); }
Коллекция типа
List
Каждый кортеж в результатах запроса, имеющий вид массива Object[], должен быть преобразован в нужный объект. Здесь по индексу из массива извлекается каждый элемент проекции и выполняется вызов фабрики ItemSummaryFactory для получения возвращаемого объекта. Hibernate также передает методу список псевдонимов для каждого элемента проекции. Но для данного преобразователя псевдонимы не требуются. Вы можете обернуть или модифицировать получившийся список результатов после преобразования результатов запроса. Здесь мы сделали возвращаемый список List неизменяемым: это отлично подходит для отчета, где никакие данные не должны меняться.
Как показано в примере, преобразование происходит в два этапа: сначала преобразуется каждый возвращаемый кортеж в нужный объект. Затем в вашем распоряжении оказывается целый список List этих объектов, который можно обернуть или преобразовать. Далее мы обсудим другую удобную особенность Hibernate (не имеющей эквивалента в JPA): фильтры коллекций. Особенности Hibernate
16.2. Фильтрация коллекций В главе 7 вы узнали, когда следует (а скорее, не следует) отображать коллекции в предметной модели на Java. Самая большая польза от отображения коллекций – более удобный доступ к данным: вы можете вызывать методы item.getImages() или item.getBids(), чтобы получить доступ ко всем изображениям или ставкам, связанным с данным товаром Item. При этом не нужно писать ни запросов JPQL, ни запросов на основе критериев; Hibernate сделает это за вас, как только вы начнете обход элементов коллекции. Самая очевидная проблема такого подхода: Hibernate будет всегда выполнять один и тот же запрос, извлекая все изображения и ставки для товара Item. Вы мо-
Фильтрация коллекций 473 жете настроить порядок элементов коллекции, но только в статическом отображении. А что делать, если требуется вывести два списка ставок для товара Item, упорядоченных по возрастанию и по убыванию? Вы могли бы вернуться к созданию собственных запросов и не вызывать метода item.getBids(), но тогда отображение коллекции может не понадобиться. Вместо этого вы можете использовать нестандартную особенность Hibernate – фильтры коллекций, – которая упрощает создание таких запросов с помощью отображаемых коллекций. Предположим, что у вас есть в памяти хранимый экземпляр Item, возможно, загруженный с помощью EntityManager. Пусть требуется отобразить все ставки (коллекцию bids) для данного товара Item, а затем наложить на коллекцию bids ограничение – выбрать только ставки, сделанные конкретным пользователем User. Также требуется отсортировать список в порядке убывания значений Bid#amount. Листинг 16.2 Фильтрация и упорядочение коллекции Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ FilterCollections.java Item item = em.find(Item.class, ITEM_ID); User user = em.find(User.class, USER_ID); org.hibernate.Query query = session.createFilter( item.getBids(), "where this.bidder = :bidder order by this.amount desc" ); query.setParameter("bidder", user); List bids = query.list();
Метод session.createFilter() принимает хранимую коллекцию и фрагмент запроса JPQL. Этот фрагмент не должен содержать предложений select и from – только ограничения в предложениях where и order by. Псевдоним this ссылается на элемент коллекции, в данном случае на экземпляр Bid. Созданный фильтр – это обычный запрос org.hibernate.Query со связанным параметром, который можно выполнить, вызвав метод list(). Hibernate не выполняет фильтрацию коллекций в памяти приложения. Если во время вызова фильтра коллекция Item#bids не была инициализирована, она такой и останется. Более того, фильтры нельзя применять к временным коллекциям и результатам запроса. Их можно применять только к отображаемым коллекциям, на которые ссылаются экземпляры сущностей, управляемые контекстом хранения. Термин фильтр в некотором роде вводит в заблуждение, поскольку результатом фильтрации будет другая, совершенно новая коллекция; исходная коллекция никак не изменится. Ко всеобщему удивлению (включая разработчиков данной функциональности), даже простейшие фильтры могут оказаться полезными. К примеру, пустой запрос можно использовать для постраничного вывода элементов коллекции:
474 Дополнительные возможности запросов Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ FilterCollections.java Item item = em.find(Item.class, ITEM_ID); org.hibernate.Query query = session.createFilter( item.getBids(), "" ); Вернет только две ставки query.setFirstResult(0); query.setMaxResults(2); List bids = query.list();
Здесь Hibernate выполнит запрос, загрузив только два элемента коллекции, начиная с первой строки результата запроса. Обычно вместе с постраничным выводом применяется и упорядочение order by. В фильтрах коллекций не требуется использовать предложения from, но вы можете добавить его, если это соответствует вашему стилю. Фильтры коллекций могут даже не возвращать элементов фильтруемой коллекции. Следующий фильтр возвращает товар Item, проданный любым из пользователей, сделавших ставку: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ FilterCollections.java Item item = em.find(Item.class, ITEM_ID); org.hibernate.Query query = session.createFilter( item.getBids(), "from Item i where i.seller = this.bidder" ); List items = query.list();
Используя предложение select, можно определить проекцию. Следующий фильтр извлекает имена всех пользователей, сделавших ставки: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ FilterCollections.java Item item = em.find(Item.class, ITEM_ID); org.hibernate.Query query = session.createFilter( item.getBids(), "select distinct this.bidder.username order by this.bidder.username asc" ); List bidders = query.list();
Все это очень интересно, но основная причина существования фильтров коллекций – в том, что они позволяют извлекать элементы коллекции без ее ини циализации. Для больших коллекций крайне важно добиться хорошей производительности. Следующий запрос извлекает из коллекции bids все ставки, сделанные за товар Item, которые больше или равны 100:
Интерфейс запросов на основе критериев в Hibernate 475 Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ FilterCollections.java Item item = em.find(Item.class, ITEM_ID); org.hibernate.Query query = session.createFilter( item.getBids(), "where this.amount >= :param" ); query.setParameter("param", new BigDecimal(100)); List bids = query.list();
Этот код не инициализирует коллекцию Item#bids, но возвращает новую коллекцию. До появления JPA 2 запросы на основе критериев были доступны лишь в виде нестандартного API в Hibernate. Сегодня стандартные интерфейсы JPA обладают не меньшей мощностью, чем старый org.hibernate.Criteria, поэтому он редко бывает нужен. Но есть несколько особенностей, которые доступны только в Hibernate, такие как запросы по образцу и возможность встраивания произвольных фрагментов SQL. В следующем разделе приводится краткий обзор интерфейса org.hibernate.Criteria и некоторых его уникальных возможностей.
Особенности Hibernate
16.3. Интерфейс запросов на основе критериев в Hibernate Используя интерфейсы org.hibernate.Criteria и org.hibernate.Example, можно создавать запросы программно, создавая и объединяя экземпляры org.hibernate. criterion.*. Далее вы увидите, как использовать эти интерфейсы и как с их по мощью определять выборки, ограничения, соединения и проекции. Мы предполагаем, что вы уже прочли предыдущую главу и знаете, как эти операции транслируются в код SQL. Для всех запросов, показанных здесь, в предыдущей главе можно найти эквивалентный пример, так что при необходимости вы сможете сравнить все три API. Начнем с самых простых примеров.
16.3.1. Выборка и упорядочение Следующий запрос загружает все экземпляры Item: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java org.hibernate.Criteria criteria = session.createCriteria(Item.class); List items = criteria.list();
476 Дополнительные возможности запросов Здесь с помощью Session создается экземпляр org.hibernate.Criteria. Также можно создать отсоединенный запрос DetachedCriteria, не связанный с открытым контекстом хранения: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java DetachedCriteria criteria = DetachedCriteria.forClass(Item.class); List items = criteria.getExecutableCriteria(session).list();
Когда понадобится выполнить этот запрос, «присоедините» его к сеансу Session, вызвав метод getExecutableCriteria(). Обратите внимание, что такая возможность поддерживается только в API запросов на основе критериев Hibernate. При работе с JPA понадобится как минимум объект EntityManagerFactory, чтобы получить объект CriteriaBuilder. Имеется возможность упорядочить результаты, как в предложении order by в JPQL. Следующий запрос загрузит все экземпляры User, упорядочив их по имени и фамилии в порядке возрастания: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java List users = session.createCriteria(User.class) .addOrder(Order.asc("firstname")) .addOrder(Order.asc("lastname")) .list();
В данном примере используется прием объединения вызовов методов в цепочку; такие методы, как addOrder(), возвращают исходный объект org.hibernate.Criteria. Далее мы рассмотрим, как ограничить выбираемые записи.
16.3.2. Ограничения Следующий запрос возвращает все экземпляры Item, свойство name которых содержит строку «Foo»: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java List items = session.createCriteria(Item.class) .add(Restrictions.eq("name", "Foo")) .list();
Интерфейс Restrictions – это фабрика объектов Criterion, которые можно добавлять в объект Criteria. Ссылка на атрибуты производится с помощью обычных строк, как, например, "name" для Item#name. Также можно организовать поиск подстроки, как в операторе like в JPQL. Следующий запрос найдет всех пользователей (экземпляры User), имена которых (username) начинаются с «j» или «J»:
Интерфейс запросов на основе критериев в Hibernate 477 Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java List users = session.createCriteria(User.class) .add(Restrictions.like("username", "j", MatchMode.START).ignoreCase()) .list();
Параметр MatchMode.START является эквивалентом шаблона j% в JPQL. Также доступны режимы EXACT, END и ANYWHERE. С помощью точечной нотации можно обращаться к вложенным атрибутам встраиваемых типов, таким как Address объекта User. Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java List users = session.createCriteria(User.class) .add(Restrictions.eq("homeAddress.city", "Some City")) .list();
Отличительной особенностью Criteria API в Hibernate является возможность добавлять в ограничения фрагменты на языке SQL. Следующий запрос найдет всех пользователей (экземпляры User), имена которых (username) состоят менее, чем из восьми символов: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java List users = session.createCriteria(User.class) .add(Restrictions.sqlRestriction( "length({alias}.USERNAME) < ?", 8, StandardBasicTypes.INTEGER )).list();
Hibernate отправит этот фрагмент SQL в базу данных без изменений. Символ подстановки {alias} нужен для передачи псевдонима таблицы в итоговом запросе SQL; он всегда ссылается на отображаемую таблицу корневой сущности (USERS в данном случае). Здесь также используется позиционный параметр (именованные не поддерживаются), тип которого определяется значением StandardBasicTypes.INTEGER. Расширение системы критериев Hibernate Система запросов на основе критериев в Hibernate допускает расширение: можно, к примеру, обернуть вызов SQL-функции LENGTH() своей реализацией интерфейса org.hibernate.criterion.Criterion.
478 Дополнительные возможности запросов После создания выборки и ограничений нужно добавить в запрос проекцию для описания возвращаемых данных.
16.3.3. Проекция и агрегирование Следующий запрос возвращает кортежи, включающие идентификатор, имя пользователя (username) и домашний адрес (homeAddress) для всех пользователей (User): Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java List result = session.createCriteria(User.class) .setProjection(Projections.projectionList() .add(Projections.property("id")) .add(Projections.property("username")) .add(Projections.property("homeAddress")) ).list();
Результатом этого запроса будет список (List) элементов типа Object[], по одному массиву на каждый кортеж. Каждый массив содержит элемент типа Long (или типа, заданного для идентификатора пользователя), String и Address. Как и при работе с ограничениями, для преобразования элементов проекции можно использовать произвольные выражения SQL и функции: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java List result = session.createCriteria(Item.class) .setProjection(Projections.projectionList() .add(Projections.sqlProjection( "NAME || ':' || AUCTIONEND as RESULT", new String[]{"RESULT"}, new Type[]{StandardBasicTypes.STRING} )) ).list();
Этот запрос вернет список (List) строк (String), где каждая строка имеет вид «[Имя товара]:[Дата окончания аукциона]». Второй параметр в проекции – псевдонимы, используемые в запросе: они необходимы Hibernate, чтобы прочитать значения из объекта ResultSet. Также необходимо указать тип каждого элемента проекции/псевдонима: здесь это StandardBasicTypes.STRING. Hibernate поддерживает группировку и агрегирование. Следующий запрос подсчитает фамилии пользователей: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java List result = session.createCriteria(User.class)
Интерфейс запросов на основе критериев в Hibernate 479 .setProjection(Projections.projectionList() .add(Projections.groupProperty("lastname")) .add(Projections.rowCount()) ).list();
Метод rowCount() действует подобно функции count() в JPQL. Следующий запрос использует агрегирование, чтобы подсчитать среднюю ставку (Bid) для каждого товара (Item): Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java List result = session.createCriteria(Bid.class) .setProjection(Projections.projectionList() .add(Projections.groupProperty("item")) .add(Projections.avg("amount")) ).list();
Далее вы узнаете, что Criteria API позволяет также выполнять соединения.
16.3.4. Соединения Внутренние соединения связанных сущностей задаются с помощью вложенных объектов Criteria: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java List result = session.createCriteria(Bid.class) .createCriteria("item") .add(Restrictions.isNotNull("buyNowPrice")) .createCriteria("seller") .add(Restrictions.eq("username", "johndoe")) .list();
Внутреннее соединение
Запрос вернет все ставки (экземпляры Bid) каждого товара (Item) с непус тым полем buyNowPrice, проданного пользователем (User) по имени «johndoe». Первое внутреннее соединение по связи Bid#item задается вызовом метода createCriteria("item") корневого объекта Criteria, созданного для класса Bid. Вложенный объект Criteria определяет путь к связи, для которой выполняется еще одно внутреннее соединение путем вызова createCriteria("seller"). На оба соединения наложены ограничения; они объединяются логическим «И» в предложении where итогового запроса SQL. Внутренние соединения также можно выразить с помощью метода createAlias() объекта Criteria. Ниже предствавлен аналогичный запрос: List result = session.createCriteria(Bid.class)
480 Дополнительные возможности запросов .createCriteria("item") Внутреннее .createAlias("seller", "s") соединение .add(Restrictions.and( Restrictions.eq("s.username", "johndoe"), Restrictions.isNotNull("buyNowPrice") )) .list();
Немедленное динамическое извлечение с помощью внешнего соединения задается с помощью метода setFetchMode(): Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java List result = session.createCriteria(Item.class) .setFetchMode("bids", FetchMode.JOIN) .list();
Этот запрос вернет все экземпляры Item с коллекциями bids, инициализированными в этом же запросе SQL. Остерегайтесь дубликатов Так же, как при работе с запросами JPQL и запросами на основе критериев JPA, фреймворк Hibernate может возвращать дубликаты объектов Item. См. обсуждение этого феномена в разделе 15.4.5.
Точно так же, как в запросах JPQL и запросах на основе критериев JPA, Hibernate может удалять дубликаты в памяти с помощью операции «distinct»: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java List result = session.createCriteria(Item.class) .setFetchMode("bids", FetchMode.JOIN) .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY) .list();
Здесь видно, как преобразователь ResultTransformer, о котором мы говорили ранее в этой главе, может применяться к объекту Criteria. Вы можете извлечь несколько связей/коллекций в одном запросе: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java List result = session.createCriteria(Item.class) .createAlias("bids", "b", JoinType.LEFT_OUTER_JOIN) .setFetchMode("b", FetchMode.JOIN)
Интерфейс запросов на основе критериев в Hibernate 481 .createAlias("b.bidder", "bdr", JoinType.INNER_JOIN) .setFetchMode("bdr", FetchMode.JOIN) .createAlias("seller", "s", JoinType.LEFT_OUTER_JOIN) .setFetchMode("s", FetchMode.JOIN) .list();
Этот запрос вернет все экземпляры Item, загрузит для каждого коллекцию Item#bids, используя внешнее соединение, а потом загрузит поле Bid#bidder, используя внутреннее соединение. Также будет загружено поле Item#seller: поскольку оно не может принимать значения null, для его загрузки можно использовать любое соединение. Только не загружайте несколько коллекций в одном запросе, иначе получится декартово произведение (см. раздел 15.4.5). Далее вы увидите, как с помощью вложенных объектов Criteria описываются подзапросы.
16.3.5. Подзапросы Следующий подзапрос вернет всех пользователей (экземпляры User), продающих более одного товара: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java DetachedCriteria sq = DetachedCriteria.forClass(Item.class, "i"); sq.add(Restrictions.eqProperty("i.seller.id", "u.id")); sq.setProjection(Projections.rowCount()); List result = session.createCriteria(User.class, "u") .add(Subqueries.lt(1l, sq)) .list();
Экземпляр DetachedCriteria описывает запрос, возвращающий количество товаров, проданных данным пользователем (User). Поскольку ограничение зависит от псевдонима u, это коррелированный подзапрос. «Внешний» запрос, включающий объект DetachedCriteria, подставит реальное значение псевдонима u. Обратите внимание, что подзапрос является правым операндом оператора lt() (меньше, чем), который преобразуется в код SQL: 1 < ([Количество результатов запроса]). В Hibernate также можно использовать кванторы. Например, следующий запрос найдет товары со ставками не выше 10: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java DetachedCriteria sq = DetachedCriteria.forClass(Bid.class, "b"); sq.add(Restrictions.eqProperty("b.item.id", "i.id")); sq.setProjection(Projections.property("amount")); List result = session.createCriteria(Item.class, "i") .add(Subqueries.geAll(new BigDecimal(10), sq)) .list();
482 Дополнительные возможности запросов И снова позиция операнда говорит о том, что сравнение основано на операторе geAll() (больше или равно каждому), который найдет все ставки, не превышающие 10. Итак, у вас уже есть несколько причин для использования org.hibernate.Criteria API. Тем не менее в новых приложениях лучше всего использовать стандартизованные языки запросов JPA. Самой интересной особенностью старого API является возможность встраивания выражений SQL в ограничения и проекции. Другой интересной особенностью Hibernate являются запросы по образцу.
16.3.6. Запросы по образцу Идея запросов по образцу заключается в передаче экземпляра сущности фреймворку Hibernate, который в ответ должен загрузить все экземпляры сущностей, похожие на образец. Это может пригодиться в пользовательском интерфейсе, где имеется сложный экран с настройками, поскольку отпадает необходимость создавать дополнительные классы для хранения поисковых запросов. Предположим, что в приложении есть форма, позволяющая выполнить поиск пользователей (экземпляры User) по фамилии. Вы можете связать поле «фамилия» в форме со свойством User#lastname и попросить Hibernate загрузить «похожих» пользователей (экземпляры User): Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java User template = new User(); template.setLastname("Doe");
Создание пустого экземпляра User
org.hibernate.criterion.Example example = Example.create(template); example.ignoreCase(); Создание экземпляра example.enableLike(MatchMode.START); Example example.excludeProperty("activated"); Игнорировать статус активности List users = session.createCriteria(User.class) .add(example) .list();
Добавить объект Example в качестве ограничения
Создать «пустой» экземпляр User, который будет играть роль образца, установив искомые значения свойств – фамилию «Doe». На основе образца создать экземпляр Example. Этот класс позволит точнее настроить поиск. Здесь используется поиск подстроки и игнорируется регистр символов, поэтому критериям поиска будет соответствовать строка «Doe», «doeX» или «Doe Y». Класс User имеет поле activated типа boolean. Поскольку простой тип не может принимать значения null, его значением по умолчанию будет false; поэтому Hibernate включит в круг поиска неактивных пользователей. Но, поскольку нужно найти всех пользователей, следует сообщить Hibernate, чтобы он это поле проигнорировал. Объект Example добавляется как ограничение в запрос, представленный объектом Cri teria.
Интерфейс запросов на основе критериев в Hibernate 483 Поскольку класс сущности User написан в соответствии с соглашением JavaBean, связать его с интерфейсом пользователя (UI) проще простого. Он имеет необходимые методы чтения/записи, а создать «пустой» экземпляр можно, используя общедоступный конструктор без аргументов (см. обсуждение проектирования конструкторов в разделе 3.2.3). Очевидным недостатком Example является применение настроек сравнения строк, таких как ignoreCase() и enableLike(), ко всем строковым свойствам образца. Если бы мы одновременно искали имя (firstname) и фамилию (lastname), игнорирование регистра символов выполнялось бы для обоих свойств. По умолчанию в ограничения запроса добавляются все свойства образца сущности, значение которых отлично от null. Как было показано в последнем примере, можно вручную исключить из поиска свойства образца, используя метод excludeProperty(). Аналогично, с помощью метода excludeZeroes(), можно исключить свойства с нулевыми значениями (типа int или long) или отменить любые исключения с помощью метода excludeNone(). В последнем случае любые свойства образца, имеющие значение null, также будут добавлены в ограничение SQLзапроса в виде проверки is null. Если нужен более полный контроль за включением и исключением свойств, можно расширить класс Example, используя собственный селектор PropertySelector: Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java class ExcludeBooleanExample extends Example { ExcludeBooleanExample(Object template) { super(template, new PropertySelector() { @Override public boolean include(Object propertyValue, String propertyName, Type type) { return propertyValue != null && !type.equals(StandardBasicTypes.BOOLEAN); } }); } }
Этот селектор исключает любые свойства со значением null (так же, как селектор по умолчанию), но, кроме этого, исключает логические свойства (такие как User#activated). После добавления образца Example в объект Criteria в виде ограничения можно определить дополнительные ограничения. Также можно создать запрос на основе нескольких образцов. Следующий запрос вернет все товары (экземпляры Item) с названиями (name), начинающимся с «B» или «b», и свойством seller, совпа дающим с объектом User:
484 Дополнительные возможности запросов Файл: /examples/src/test/java/org/jpwh/test/querying/advanced/ HibernateCriteria.java Item itemTemplate = new Item(); itemTemplate.setName("B"); Example exampleItem = Example.create(itemTemplate); exampleItem.ignoreCase(); exampleItem.enableLike(MatchMode.START); exampleItem.excludeProperty("auctionType"); exampleItem.excludeProperty("createdOn"); User userTemplate = new User(); userTemplate.setLastname("Doe"); Example exampleUser = Example.create(userTemplate); exampleUser.excludeProperty("activated"); List items = session .createCriteria(Item.class) .add(exampleItem) .createCriteria("seller").add(exampleUser) .list();
А теперь представьте, сколько кода SQL/JDBC потребовалось бы написать вручную, чтобы создать такой поисковый запрос.
16.4. Резюме Вы познакомились с использованием ResultTransformer для преобразования результатов запроса и узнали, как вернуть список списков, список словарей и как отобразить псевдонимы в свойства сущностей. Мы рассмотрели механизм фильтрации коллекций в Hibernate, помогающий упростить взаимодействия с хранимыми коллекциями. Вы познакомились с интерфейсом запросов Criteria в Hibernate и узнали, в каких ситуациях он может заменить стандартные запросы на основе критериев JPA. Мы рассмотрели все возможности данного API: выборки и упорядочение, проекции и агрегирование, а также соединения, подзапросы и запросы на основе образцов.
Глава
17 Настройка SQL-запросов
В этой главе: назад к JDBC; отображение результатов SQL-запросов; настройка операций CRUD; вызов хранимых процедур.
В этой главе мы рассмотрим настройку SQL-запросов и способы их интеграции с приложениями Hibernate. Язык SQL был создан в 1970 году, но ANSI (American National Standard Institute – Американский институт стандартов) стандартизовал его лишь в 1986-м. Несмотря на то что каждое обновление стандарта SQL добавляло новые возможности (иногда довольно противоречивые), все СУБД поддерживают SQL по-своему. Заботы о переносимости лежат на плечах разработчиков баз данных. И здесь на помощь приходит Hibernate: его встроенный язык запросов генерирует код SQL в соответствии с настроенным диалектом базы данных. Диалекты также влияют на весь автоматически генерируемый код SQL (например, используемый для извлечения коллекции по требованию). Простой заменой диалекта можно интегрировать свое приложение с другой СУБД. Hibernate генерирует все выражения SQL за вас: операции создания, чтения, изменения и удаления (CRUD). Но иногда может потребоваться нечто большее, чем могут дать Hibernate и Java Persistence API, и тогда приходится спускаться на более низкий уровень абстрации. Используя Hibernate, можно выполнять произвольные выражения SQL: можно вернуться к использованию JDBC API и работать напрямую с интерфейсами Connection, PreparedStatement и ResultSet. В Hibernate имеется интерфейс Connection, поэтому вам не придется управлять отдельным пулом соединений, и все ваши выражения SQL будут выполняться в рамках одной (текущей) транзакции; можно писать SQL-выражения SELECT, встраивая их в Java-код или сохраняя в отдельном XML-файле или аннотациях в виде именованных запросов. Эти SQL-запросы будут выполняться с помощью Java Persistence API, как обычные запросы JPQL. Hibernate сможет преобразовать результаты в со-
486 Настройка SQL-запросов ответствии с заданным отображением. Этот прием можно также использовать для вызова хранимых процедур; SQL-выражения, сгенерированные фреймворком Hibernate, можно заменить своими собственными, написанными вручную. Это значит, что при загрузке экземпляра сущности методом em.find() или при загрузке коллекции по требованию данные будет извлекать ваш собственный запрос SQL. Можно даже написать собственный язык управления данными (Data Manipulation Language, DML), включающий такие инструкции, как UPDATE, INSERT и DELETE. А также для выполнения операций CRUD можно вызывать хранимые процедуры. Все автоматически сгенерированные выражения SQL можно заменить собственным кодом. Мы начнем эту главу с использования обычного JDBC, а затем обсудим возможности Hibernate по отображению результатов запроса. Потом покажем, как в Hibernate переопределяются запросы и DML-операции. И в завершение расскажем об интеграции с хранимыми процедурами в базе данных. Главные нововведения в JPA 2 • Результаты SQL-запросов можно отображать в вызовы конструкторов. • С помощью StoredProcedureQuery можно вызывать хранимые процедуры и функции непосредственно.
Особенности Hibernate
17.1. Назад к JDBC Иногда бывает нужно обращаться к базе данных напрямую, с помощью JDBC API и минуя Hibernate. Для этого вам понадобится интерфейс java.sql.Connection, с помощью которого можно создать и выполнить запрос (объект PreparedStatement), а также напрямую обратиться к результатам запроса (объекту ResultSet). Поскольку Hibernate уже знает, как создавать и закрывать соединение с базой данных, он может передать вашему приложению объект Connection и автоматически освободить его, когда надобность в нем отпадет. Эта функциональность доступна благодаря интерфейсу, org.hibernate.jdbc. Work, основанному на обратных вызовах. Все взаимодействие с JDBC заключено в реализации этого интерфейса; Hibernate вызовет ее, передав объект Connection. Следующий пример выполняет SQL-запрос SELECT и производит обход результатов в коллекции ResultSet. Листинг 17.1 Инкапсуляция взаимодействия с интерфейсами JDBC Файл: /examples/src/test/java/org/jpwh/test/querying/sql/JDBCFallback.java public class QueryItemWork implements org.hibernate.jdbc.Work { final protected Long itemId;
Идентикатор экземпляра Item
Назад к JDBC 487 public QueryItemWork(Long itemId) { this.itemId = itemId; } Вызов метода execute() @Override public void execute(Connection connection) throws SQLException { PreparedStatement statement = null; ResultSet result = null; try { statement = connection.prepareStatement( "select * from ITEM where ID = ?" ); statement.setLong(1, itemId);
result = statement.executeQuery();
}
}
while (result.next()) { String itemName = result.getString("NAME"); BigDecimal itemPrice = result.getBigDecimal("BUYNOWPRICE"); // ... } } finally { Освобождение ресурсов if (result != null) result.close(); if (statement != null) statement.close(); }
Для выполнения этого запроса нужен идентификатор товара, поэтому в классе определено финальное поле, устанавливаемое через параметр конструктора. Hibernate вызывает метод execute() и передает ему объект Connection. Вам не нужно закрывать соединение по завершении работы. Но не забудьте освободить остальные ресурсы, такие как PreparedStatement и ResultSet.
Выполнить работу, представленную объектом Work, можно с помощью Session: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/JDBCFallback.java UserTransaction tx = TM.getUserTransaction(); tx.begin(); EntityManager em = JPA.createEntityManager(); Session session = em.unwrap(Session.class); session.doWork(new QueryItemWork(ITEM_ID)); tx.commit(); em.close();
В данном случае соединение (Connection), возвращаемое Hibernate, будет находиться в границах текущей системной транзакции. Все ваши изменения будут
488 Настройка SQL-запросов зафиксированы с подтверждением системной транзакции, а все операции, выполняются они с помощью EntityManager или Session, будут являться частью одной единицы работы. Если вам потребуется вернуть приложению результат выполнения «работы», используйте интерфейс org.hibernate.jdbc.ReturningWork. Для JDBC-операций, выполняемых внутри реализации интерфейса Work, нет никаких ограничений. Чтобы вызвать хранимую процедуру, вместо PreparedStatement можно использовать CallableStatement; в вашем распоряжении полный доступ к JDBC API. Для более простых запросов и работы с коллекцией ResultSet, такой как в предыдущем примере, есть более удобная альтернатива.
17.2. Отображение результатов SQL-запросов После выполнения SQL-запроса SELECT с помощью JDBC API или вызова хранимой процедуры, возвращающей коллекцию ResultSet, выполняется обход записей в результатах и извлекаются нужные данные. Это довольно трудоемкая задача, требующая раз за разом писать один и тот же код. Быстрая проверка выражений SQL Для упрощения тестирования сценариев SQL с несколькими СУБД без запуска локального сервера можно воспользоваться онлайн-службой SQL Fiddle по адресу: http://sqlfiddle.com.
Hibernate предлагает альтернативное решение: выполнить обычный SQL-за прос или вызвать хранимую процедуру можно с помощью Hibernate/Java Persistence API, но вместо коллекции ResultSet этот механизм вернет список List с нужными экземплярами. Результат запроса ResultSet можно отобразить в любой класс по вашему усмотрению, а Hibernate сделает все необходимые преобразования. ЗАМЕЧАНИЕ В этом разделе мы будем говорить только о SQL-запросах SELECT. Однако тот же программный интерфейс можно использовать для выполнения запросов UPDATE и INSERT, как будет показано в разделе 20.1.
Особенности Hibernate На сегодняшний день существуют два API для выполнения запросов SQL и преобразования их результатов: стандартизированный Java Persistence API с методом EntityManager#create NativeQuery() для встроенных SQL-выражений и аннотацией @NamedNativeQuery для запросов, хранящихся в отдельных файлах. Результаты запроса можно отобразить с помощью аннотации @SqlResultSetMapping или определить отображение в JPA-файле orm.xml. Также можно поместить именованные SQL-запросы в XML-файлы JPA;
Отображение результатов SQL-запросов 489 нестандартный и более старый механизм Hibernate с методом Sessi on#createSQLQuery() и интерфейсом org.hibernate.SQLQuery для отображения результатов запроса. Также можно помещать именованные SQL-запро сы и отображения результатов в XML-файлы метаданных Hibernate. Возможности Hibernate шире. Например, он поддерживает немедленную загрузку коллекций и связей сущностей в отображениях результатов SQL-запросов. В следующих разделах мы сравним два API на примере одних и тех же запросов. Начнем с простого встроенного SQL-запроса и отображения скалярного результата проекции.
17.2.1. Проекции в SQL-запросах Следующий запрос вернет список List элементов типа Object[], где каждый элемент представляет кортеж (запись) SQL-проекции: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/NativeQueries.java Query query = em.createNativeQuery( "select NAME, AUCTIONEND from {h-schema}ITEM" ); List result = query.getResultList(); for (Object[] tuple : result) { assertTrue(tuple[0] instanceof String); assertTrue(tuple[1] instanceof Date);} Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ HibernateSQLQueries.java org.hibernate.SQLQuery query = session.createSQLQuery( "select NAME, AUCTIONEND from {h-schema}ITEM" ); List result = query.list(); for (Object[] tuple : result) { assertTrue(tuple[0] instanceof String); assertTrue(tuple[1] instanceof Date); }
Методы em.createNativeQuery() и session.createSQLQuery() могут принимать обычные запросы SQL в виде строк. В данном случае запрос извлекает значения столбцов NAME и AUCTIONEND из таб лицы ITEM, а Hibernate автоматически преобразует их в значения типа String и java.util.Date. Для определения типов конкретных элементов фреймворк Hibernate обращается к метаданным java.sql.ResultSetMetaData. Ему известно, что тип VARCHAR отображается в тип String, а TIMESTAMP – в тип java.util.Date (как объяснялось в разделе 5.3). Механизм обработки SQL-запросов в Hibernate поддерживает несколько удобных символов подстановки, таких как {h-schema} в предыдущем примере. Hibernate заменит символ подстановки схемой, указанной по умолчанию для единицы
490 Настройка SQL-запросов хранения (параметр hibernate.default_schema). В числе других символов подстановки можно назвать {h-catalog} (каталог SQL по умолчанию) и {h-domain} (объединяет значения каталога и схемы). Самое большое преимущество выполнения SQL-выражений с помощью Hibernate состоит в автоматическом преобразовании результатов в экземпляры классов предметной модели.
17.2.2. Отображение в классы сущностей Следующий запрос SQL вернет список List экземпляров сущности Item: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/NativeQueries.java Query query = em.createNativeQuery( "select * from ITEM", Item.class ); List result = query.getResultList(); Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ HibernateSQLQueries.java org.hibernate.SQLQuery query = session.createSQLQuery( "select * from ITEM" ); query.addEntity(Item.class); List result = query.list();
Полученные экземпляры Item будут находиться в хранимом состоянии под управлением текущего контекста хранения. Он вернет тот же результат, что и JPQL-запрос select i from Item i. Для данного преобразования Hibernate выполнит обход результатов SQL-за проса и попытается отыскать имена и типы столбцов в метаданных отображения. Если столбец AUCTIONEND отображается в свойство Item#auctionEnd, Hibernate будет знать, как это свойство заполнить, и вернет целиком загруженные экземпляры. Обратите внимание: Hibernate ожидает, что запрос вернет все столбцы, необходимые для создания экземпляра Item, включая свойства, встроенные компоненты и значения столбцов внешних ключей. Если Hibernate не удастся обнаружить отображаемый столбец (по имени) в результате запроса, он возбудит исключение. Чтобы получить такие же имена столбцов, как в метаданных отображения сущности, могут понадобиться псевдонимы в SQL. Оба интерфейса, javax.persistence.Query и org.hibernate.SQLQuery, поддерживают связывание параметров. Следующий запрос вернет только один экземпляр сущности Item:
Отображение результатов SQL-запросов 491 Файл: /examples/src/test/java/org/jpwh/test/querying/sql/NativeQueries.java Query query = em.createNativeQuery( "select * from ITEM where ID = ?", Item.class ); query.setParameter(1, ITEM_ID); Нумерация параметров начинается с 1 List result = query.getResultList(); Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ HibernateSQLQueries.java org.hibernate.SQLQuery query = session.createSQLQuery( "select * from ITEM where ID = ?" ); query.addEntity(Item.class); query.setParameter(0, ITEM_ID); Нумерация параметров начинается с 0 List result = query.list();
По историческим причинам Hibernate нумерует параметры, начиная с нуля, тогда как JPA – с единицы. По этой причине предпочтительнее использовать именованные параметры: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/NativeQueries.java Query query = em.createNativeQuery( "select * from ITEM where ID = :id", Item.class ); query.setParameter("id", ITEM_ID); List result = query.getResultList(); Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ HibernateSQLQueries.java org.hibernate.SQLQuery query = session.createSQLQuery( "select * from ITEM where ID = :id" ); query.addEntity(Item.class); query.setParameter("id", ITEM_ID); List result = query.list();
Несмотря на то что параметризованные запросы поддерживаются в обоих API, спецификация JPA не считает именованные параметры в обычных запросах переносимыми. Следовательно, не все реализации JPA поддерживают именованные параметры в обычных запросах. Если SQL-запрос возвращает не все столбцы, описанные в отображении класса сущности Java, и его нельзя переписать, используя псевдонимы, следует явно определить отображение результатов запроса.
492 Настройка SQL-запросов
17.2.3. Настройка отображения запросов Следующий запрос SQL вернет список List управляемых экземпляров сущности Item: все столбцы таблицы ITEM включены в SQL-проекцию, что требуется для создания экземпляров Item. Но столбец NAME в проекции переименован в EXTENDED_ NAME с помощью псевдонима: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/NativeQueries.java Query query = em.createNativeQuery( "select " + "i.ID, " + "'Auction: ' || i.NAME as EXTENDED_NAME, " + "i.CREATEDON, " + "i.AUCTIONEND, " + "i.AUCTIONTYPE, " + "i.APPROVED, " + "i.BUYNOWPRICE, " + "i.SELLER_ID " + "from ITEM i", "ItemResult" ); List result = query.getResultList();
Hibernate в этом случае не сможет автоматически сопоставить поля результата запроса со свойствами сущности Item: потому что столбца NAME больше нет. Следовательно, нужно указать способ отображения результата с помощью второго параметра метода createNativeQuery(); в данном случае ItemResult.
Отображение полей результата запроса в свойства сущности Это отображение можно настроить с помощью аннотаций, например в классе Item: Файл: /model/src/main/java/org/jpwh/model/querying/Item.java @SqlResultSetMappings({ @SqlResultSetMapping( name = "ItemResult", entities = @EntityResult( entityClass = Item.class, fields = { @FieldResult(name = "id", column = "ID"), @FieldResult(name = "name", column = "EXTENDED_NAME"), @FieldResult(name = "createdOn", column = "CREATEDON"), @FieldResult(name = "auctionEnd", column = "AUCTIONEND"), @FieldResult(name = "auctionType", column = "AUCTIONTYPE"), @FieldResult(name = "approved", column = "APPROVED"), @FieldResult(name = "buyNowPrice", column = "BUYNOWPRICE"),
Отображение результатов SQL-запросов 493
)
)
}
@FieldResult(name = "seller", column = "SELLER_ID")
}) @Entity public class Item { // ... }
Все поля в результате запроса должны отображаться в свойства класса сущности. Даже если только одно свойство/столбец (EXTENDED_NAME) не совпадает с заданным отображением, все равно нужно перечислять все свойства и столбцы. Отображения результатов запросов SQL, заданные в виде аннотаций, трудно читать, и, кроме того, аннотации JPA можно размещать только перед определением класса, а не в файле метаданных package-info.java. Мы предпочитаем хранить такие отображения в файлах XML. Ниже показано точно такое же отображение: Файл: /model/src/main/resources/querying/NativeQueries.xml
Если отображения будут называться одинаково, приоритет будет отдан отображениям из файла XML. Сам SQL-запрос можно определить с помощью аннотации @NamedNativeQuery или элемента , как было показано в разделе 14.4. В следующих примерах мы будем размещать выражения SQL в Java-коде, поскольку так вам будет проще понять его логику. Но в практике чаще встречаются отображения результатов, задаваемые с помощью более краткого синтаксиса XML. Давайте сначала повторим предыдущий запрос, используя нестандартный интерфейс Hibernate: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ HibernateSQLQueries.java org.hibernate.SQLQuery query = session.createSQLQuery( "select " + "i.ID as {i.id}, " +
494 Настройка SQL-запросов "'Auction: ' || i.NAME as {i.name}, " + "i.CREATEDON as {i.createdOn}, " + "i.AUCTIONEND as {i.auctionEnd}, " + "i.AUCTIONTYPE as {i.auctionType}, " + "i.APPROVED as {i.approved}, " + "i.BUYNOWPRICE as {i.buyNowPrice}, " + "i.SELLER_ID as {i.seller} " + "from ITEM i"
); query.addEntity("i", Item.class); List result = query.list();
В Hibernate отображение результатов запроса можно задать прямо в тексте самого запроса, используя символы подстановки с псевдонимами. Значение псевдонима i задается вызовом метода addEntity(). Теперь Hibernate сможет сгенерировать настоящие псевдонимы в проекции SQL, используя символы подстановки, такие как {i.name} и {i.auctionEnd}, указывающие на свойства сущности Item. Других определений в отображении результатов запроса не требуется; Hibernate сгенерирует псевдоним в коде SQL и сможет прочитать значения полей коллекции ResultSet. Это гораздо удобнее, чем отображать результаты запросов в JPA. Но если код SQL нельзя изменить, отображения можно определить с помощью методов addRoot() и addProperty() объекта org.hibernate.SQLQuery: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ HibernateSQLQueries.java org.hibernate.SQLQuery query = session.createSQLQuery( "select " + "i.ID, " + "'Auction: ' || i.NAME as EXTENDED_NAME, " + "i.CREATEDON, " + "i.AUCTIONEND, " + "i.AUCTIONTYPE, " + "i.APPROVED, " + "i.BUYNOWPRICE, " + "i.SELLER_ID " + "from ITEM i" ); query.addRoot("i", Item.class) .addProperty("id", "ID") .addProperty("name", "EXTENDED_NAME") .addProperty("createdOn", "CREATEDON") .addProperty("auctionEnd", "AUCTIONEND") .addProperty("auctionType", "AUCTIONTYPE") .addProperty("approved", "APPROVED") .addProperty("buyNowPrice", "BUYNOWPRICE") .addProperty("seller", "SELLER_ID"); List result = query.list();
Отображение результатов SQL-запросов 495 Так же, как в стандартном API, в Hibernate можно задать имя существующего отображения: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ HibernateSQLQueries.java org.hibernate.SQLQuery query = session.createSQLQuery( "select " + "i.ID, " + "'Auction: ' || i.NAME as EXTENDED_NAME, " + "i.CREATEDON, " + "i.AUCTIONEND, " + "i.AUCTIONTYPE, " + "i.APPROVED, " + "i.BUYNOWPRICE, " + "i.SELLER_ID " + "from ITEM i" ); query.setResultSetMapping("ItemResult"); List result = query.list();
Еще одна ситуация, когда приходится задавать собственное отображение результатов запроса, – повторение имен столбцов в результатах запроса SQL.
Отображение повторяющихся полей Следующий запрос загрузит всех продавцов (поле seller) всех товаров (экземпляров Item) в одном выражении, соединив таблицы ITEM и USERS: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/NativeQueries.java Query query = em.createNativeQuery( "select " + "i.ID as ITEM_ID, " + "i.NAME, " + "i.CREATEDON, " + "i.AUCTIONEND, " + "i.AUCTIONTYPE, " + "i.APPROVED, " + "i.BUYNOWPRICE, " + "i.SELLER_ID, " + "u.ID as USER_ID, " + "u.USERNAME, " + "u.FIRSTNAME, " + "u.LASTNAME, " + "u.ACTIVATED, " + "u.STREET, " + "u.ZIPCODE, " + "u.CITY " + "from ITEM i join USERS u on u.ID = i.SELLER_ID",
496 Настройка SQL-запросов "ItemSellerResult" ); List result = query.getResultList(); for (Object[] tuple : result) { assertTrue(tuple[0] instanceof Item); assertTrue(tuple[1] instanceof User); Item item = (Item) tuple[0]; assertTrue(Persistence.getPersistenceUtil().isLoaded(item, "seller")); assertEquals(item.getSeller(), tuple[1]); }
Фактически это немедленная загрузка связи Item#seller. Hibernate знает, что каждая запись содержит поля для экземпляров сущностей Item и User, связанных внешним ключом SELLER_ID. В данном случае в результатах запроса будут повторяться имена столбцов, соответствующих i.ID и u.ID. Им присвоены псевдонимы ITEM_ID и USER_ID, поэтому вы должны настроить отображение результата запроса: Файл: /model/src/main/resources/querying/NativeQueries.xml
Как и прежде, требуется связать все поля каждой полученной сущности с именами столбцов, даже если от первоначального отображения отличаются только два из них. Отобразить результаты такого запроса в Hibernate намного проще:
Отображение результатов SQL-запросов 497 Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ HibernateSQLQueries.java org.hibernate.SQLQuery query = session.createSQLQuery( "select " + "{i.*}, {u.*} " + "from ITEM i join USERS u on u.ID = i.SELLER_ID" ); query.addEntity("i", Item.class); query.addEntity("u", User.class); List result = query.list();
Hibernate сгенерирует уникальные псевдонимы для символов подстановки {i.*} и {u.*} в SQL-выражении, поэтому в запросе не будет одинаковых имен столбцов. В предыдущем отображении результатов запросов JPA вы, должно быть, заметили использование точечной нотации при обращении ко встроенному компоненту homeAddress экземпляра User. Давайте еще раз рассмотрим этот особый случай.
Отображение полей в свойства компонентов Класс User имеет свойство homeAddress, встроенный экземпляр класса Address. Следующий запрос загрузит всех пользователей (экземпляры User): Файл: /examples/src/test/java/org/jpwh/test/querying/sql/NativeQueries.java Query query = em.createNativeQuery( "select " + "u.ID, " + "u.USERNAME, " + "u.FIRSTNAME, " + "u.LASTNAME, " + "u.ACTIVATED, " + "u.STREET as USER_STREET, " + "u.ZIPCODE as USER_ZIPCODE, " + "u.CITY as USER_CITY " + "from USERS u", "UserResult" ); List result = query.getResultList();
В этом запросе переименованы столбцы STREET, ZIPCODE и CITY, поэтому их нужно вручную отобразить в свойства встроенного компонента: Файл: /model/src/main/resources/querying/NativeQueries.xml
498 Настройка SQL-запросов
Мы уже использовали точечную нотацию несколько раз, когда говорили о встроенных компонентах: обратиться к свойству street компонента, на который ссылается свойство homeAddress, можно как homeAddress.street. Для вложенных встроенных компонентов можно использовать выражения вроде homeAddress. city.name, если City – не простая строка, а другой встраиваемый класс. Hibernate также поддерживает точечную нотацию в символах подстановки для свойств компонентов. Ниже показан тот же самый запрос с таким же самым отображением результатов: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ HibernateSQLQueries.java org.hibernate.SQLQuery query = session.createSQLQuery( "select " + "u.ID as {u.id}, " + "u.USERNAME as {u.username}, " + "u.FIRSTNAME as {u.firstname}, " + "u.LASTNAME as {u.lastname}, " + "u.ACTIVATED as {u.activated}, " + "u.STREET as {u.homeAddress.street}, " + "u.ZIPCODE as {u.homeAddress.zipcode}, " + "u.CITY as {u.homeAddress.city} " + "from USERS u" ); query.addEntity("u", User.class); List result = query.list();
Немедленное извлечение коллекции в запросе SQL доступно только в Hibernate. Особенности Hibernate
Немедленное извлечение коллекций Предположим, что требуется загрузить все экземпляры Item в одном SQL-за просе, чтобы при этом для каждого экземпляра Item была загружена его коллекция bids. Это требует использования в запросе SQL левого внешнего соединения:
Отображение результатов SQL-запросов 499 Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ HibernateSQLQueries.java org.hibernate.SQLQuery query = session.createSQLQuery( Соединение таблиц ITEM и BID "select " + "i.ID as ITEM_ID, " + "i.NAME, " + "i.CREATEDON, " + "i.AUCTIONEND, " + "i.AUCTIONTYPE, " + "i.APPROVED, " + "i.BUYNOWPRICE, " + "i.SELLER_ID, " + "b.ID as BID_ID," + "b.ITEM_ID as BID_ITEM_ID, " + "b.AMOUNT, " + "b.BIDDER_ID " + "from ITEM i left outer join BID b on i.ID = b.ITEM_ID" ); query.addRoot("i", Item.class) Отображение столбцов в свойства сущностей .addProperty("id", "ITEM_ID") .addProperty("name", "NAME") .addProperty("createdOn", "CREATEDON") .addProperty("auctionEnd", "AUCTIONEND") .addProperty("auctionType", "AUCTIONTYPE") .addProperty("approved", "APPROVED") .addProperty("buyNowPrice", "BUYNOWPRICE") .addProperty("seller", "SELLER_ID"); query.addFetch("b", "i", "bids") Отображение свойств Bid в атрибуты результата запроса .addProperty("key", "BID_ITEM_ID") .addProperty("element", "BID_ID") .addProperty("element.id", "BID_ID") .addProperty("element.item", "BID_ITEM_ID") .addProperty("element.amount", "AMOUNT") .addProperty("element.bidder", "BIDDER_ID"); List result = query.list(); assertEquals(result.size(), 5);
5 записей в результате
for (Object[] tuple : result) { Item item = (Item) tuple[0]; Первый элемент кортежа – экземпляр Item assertTrue(Persistence.getPersistenceUtil().isLoaded(item, "bids"));
}
Bid bid = (Bid) tuple[1]; Второй элемент – экземпляр Bid if (bid != null) assertTrue(item.getBids().contains(bid));
500 Настройка SQL-запросов В запросе используется внешнее соединение таблиц ITEM и BID. В проекции определены все столбцы, необходимые для создания экземпляров Item и Bid. Повторяющиеся имена столбцов, такие как ID, в запросе заменены псевдонимами. Но по этой причине приходится отображать каждый столбец в соответствующее свойство сущности. Нужно сконструировать объект FetchReturn для коллекции bids, указав для сущностивладельца псевдоним i, а затем отобразить специальные свойства key и element в столбец внешнего ключа BID_ITEM_ID и идентификатор сущности Bid. Затем каждое свойство Bid отображается в соответствующее поле в результатах запроса. Некоторые поля отображаются дважды, что требуется Hibernate для инициализации коллекции. Число записей в результатах запроса является произведением: у одного товара три ставки, у другого – одна ставка, у последнего их нет вовсе, что даст в результате пять кор тежей. Первым элементом каждого кортежа является экземпляр Item с инициализированной коллекцией bids. Вторым элементом является экземпляр Bid.
Если имена столбцов в запросе SQL совпадают с именами в существующем отображении, можно не отображать результата запроса. В этом случае Hibernate самостоятельно сгенерирует нужные псевдонимы в запросе SQL, используя символы подстановки: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ HibernateSQLQueries.java org.hibernate.SQLQuery query = session.createSQLQuery( "select " + "{i.*}, " + "{b.*} " + "from ITEM i left outer join BID b on i.ID = b.ITEM_ID" ); query.addEntity("i", Item.class); query.addFetch("b", "i", "bids"); List result = query.list();
Немедленное извлечение коллекций в запросах SQL доступно только в Hibernate; эта функциональность не стандартизована в JPA. Ограничения при загрузке коллекций в запросах SQL Используя org.hibernate.SQLQuery, можно извлекать только коллекции, представленные связями один ко многим и многие ко многим. На момент написания книги Hibernate не поддерживал отображения результатов в коллекции простых или встраиваемых типов. Это означает, что невозможно загрузить коллекцию Item#images, используя произвольный запрос SQL и org.hibernate.SQLQuery.
Отображение результатов SQL-запросов 501 До сих пор вы видели, как запросы SQL возвращают управляемые экземпляры сущностей. Но точно так же можно возвращать временные экземпляры любого класса, вызывая нужный конструктор.
Отображение результатов запроса в параметры конструктора В разделе 15.3.2 мы рассмотрели динамическое создание экземпляров в запросах JPQL и запросах на основе критериев. Похожая функциональность поддерживается для обычных запросов JPA. Следующий запрос вернет список List экземпляров ItemSummary: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/NativeQueries.java Query query = em.createNativeQuery( "select ID, NAME, AUCTIONEND from ITEM", "ItemSummaryResult" ); List result = query.getResultList();
Отображение ItemSummaryResult преобразует каждый столбец результата запроса в соответствующий параметр конструктора ItemSummary: Файл: /model/src/main/resources/querying/NativeQueries.xml
Типы возвращаемых столбцов должны соответствовать типам параметров конструктора; по умолчанию Hibernate выберет для столбца ID тип BigInteger, поэтому нужно указать для него тип Long с помощью атрибута class. Hibernate предоставляет вам выбор. Вы можете указать имя существующего отображения результатов запроса или применить преобразователь результатов, как было показано в разделе 16.1 для запросов JPQL: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ HibernateSQLQueries.java org.hibernate.SQLQuery query = session.createSQLQuery( "select ID, NAME, AUCTIONEND from ITEM" ); // query.setResultSetMapping("ItemSummaryResult"); query.addScalar("ID", StandardBasicTypes.LONG); query.addScalar("NAME"); query.addScalar("AUCTIONEND");
Использовать существующее отображение
Отобразить поля как скаляры
502 Настройка SQL-запросов query.setResultTransformer( Применить преобразователь результатов new AliasToBeanConstructorResultTransformer( ItemSummary.class.getConstructor( Long.class, String.class, Date.class ) ) ); List result = query.list(); Есть возможность использовать существующее отображение. С другой стороны, можно отобразить поля, возвращаемые запросом, как скаляры. Без применения преобразователя результатов для каждой записи вы получили бы массив Object[]. Применив встроенный преобразователь, можно превратить массив Object[] в экземпляр ItemSummary.
Как было показано в разделе 15.3.2, Hibernate может использовать любой конструктор для такого отображения. Например, вместо экземпляров ItemSummary можно было бы создать экземпляры Item. Они будут находиться во временном или в отсоединенном состоянии, в зависимости от присутствия идентификатора в результатах запроса и в отображении. Также можно смешивать различные виды отображений результатов запроса или напрямую возвращать скалярные значения.
Скалярные и смешанные отображения Следующий запрос вернет список List массивов Object[], первым элементом в которых будет экземпляр сущности Item (товар), а вторым – скаляр, представляющий число ставок за каждый товар: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/NativeQueries.java Query query = em.createNativeQuery( "select " + "i.*, " + "count(b.ID) as NUM_OF_BIDS " + "from ITEM i left join BID b on b.ITEM_ID = i.ID " + "group by i.ID, i.NAME, i.CREATEDON, i.AUCTIONEND, " + "i.AUCTIONTYPE, i.APPROVED, i.BUYNOWPRICE, i.SELLER_ID", "ItemBidResult" ); List result = query.getResultList(); for (Object[] tuple : result) { assertTrue(tuple[0] instanceof Item); assertTrue(tuple[1] instanceof Number); }
Отображение результатов SQL-запросов 503 Поскольку повторяющиеся имена столбцов отсутствуют, отображение выглядит просто: Файл: /model/src/main/resources/querying/NativeQueries.xml
В Hibernate можно добавить дополнительное скалярное поле, вызвав метод addScalar(): Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ HibernateSQLQueries.java org.hibernate.SQLQuery query = session.createSQLQuery( "select " + "i.*, " + "count(b.ID) as NUM_OF_BIDS " + "from ITEM i left join BID b on b.ITEM_ID = i.ID " + "group by i.ID, i.NAME, i.CREATEDON, i.AUCTIONEND, " + "i.AUCTIONTYPE, i.APPROVED, i.BUYNOWPRICE, i.SELLER_ID" ); query.addEntity(Item.class); query.addScalar("NUM_OF_BIDS"); List result = query.list(); for (Object[] tuple : result) { assertTrue(tuple[0] instanceof Item); assertTrue(tuple[1] instanceof Number); }
Наконец, в одном отображении можно совмещать сущности, конструкторы и скаляры. Следующий запрос вернет управляемый хранимый экземпляр сущности User, представляющий продавца (поле seller) возвращаемого товара ItemSummary. Также вы получите количество ставок за каждый товар: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/NativeQueries.java Query query = em.createNativeQuery( "select " + "u.*, " + "i.ID as ITEM_ID, i.NAME as ITEM_NAME, i.AUCTIONEND as ITEM_AUCTIONEND, " + "count(b.ID) as NUM_OF_BIDS " + "from ITEM i " + "join USERS u on u.ID = i.SELLER_ID " + "left join BID b on b.ITEM_ID = i.ID " + "group by u.ID, u.USERNAME, u.FIRSTNAME, u.LASTNAME, " + "u.ACTIVATED, u.STREET, u.ZIPCODE, u.CITY, " +
504 Настройка SQL-запросов
);
"ITEM_ID, ITEM_NAME, ITEM_AUCTIONEND", "SellerItemSummaryResult"
List result = query.getResultList(); for (Object[] tuple : result) { assertTrue(tuple[0] instanceof User); assertTrue(tuple[1] instanceof BigInteger); assertTrue(tuple[2] instanceof ItemSummary); }
Неправильный порядок результатов: ошибка в Hibernate с номером HHH-8678
Для этого запроса задано следующее отображение: Файл: /model/src/main/resources/querying/NativeQueries.xml
Спецификация JPA гарантирует, что в смешанных отображениях результатов запросов каждый кортеж Object[] будет содержать следующие элементы: сначала все отображения , затем и, наконец, . XML-схема JPA гарантирует этот порядок в объявлении отображения; но даже если отобразить элементы в другом порядке с помощью аннотаций (которые не могут обеспечить порядка отображений), результат запроса будет сохранять стандартный порядок. Но помните, что на момент написания книги Hibernate возвращал результаты в неправильном порядке, как было показано в примере выше. Обратите внимание, что с помощью Hibernate можно использовать это же отображение запроса, указав его имя, как было показано ранее. Если требуется более полный контроль над преобразованием результатов, создайте собственный преобразователь результатов, если, конечно, вы не найдете подходящего встроенного преобразователя. В завершение вы увидите более сложный пример запроса SQL, объявленного в файле XML.
17.2.4. Размещение обычных запросов в отдельных файлах Сейчас мы покажем, как объявить запрос SQL в файле XML. В настоящих приложениях с большими запросами SQL не очень удобно читать строки в Java-коде, поэтому вам будет проще хранить запросы SQL в файлах XML. Это также упрос тит тестирование, поскольку можно копировать и вставлять выражения SQL из файла XML в консоль базы данных SQL.
Отображение результатов SQL-запросов 505 Вы наверняка обратили внимание, что все примеры запросов SQL в предыдущем разделе были тривиально простыми. Фактически ни один из примеров не требовал применения SQL – в каждом случае можно было бы использовать JPQL. Чтобы сделать следующий пример более интересным, мы напишем запрос, который нельзя выразить на языке JPQL, – только на SQL.
Дерево категорий Рассмотрим класс Category (категория) и связь многие к одному, которая ссылается сама на себя, как показано на рис. 17.1. Это отображение – обычная связь с аннотацией @ManyToOne перед столбцом внешнего ключа PARENT_ID: Файл: /model/src/main/java/org/jpwh/model/querying/Category.java @Entity public class Category { @ManyToOne @JoinColumn( name = "PARENT_ID", foreignKey = @ForeignKey(name = "FK_CATEGORY_PARENT_ID") ) Корневая категория не имеет родителя; столбец может содержать null protected Category parent; }
// ...
Рис. 17.1 Сущность Category ссылается сама на себя с помощью связи многие к одному
Категории образуют дерево. Корнем дерева является категория (экземпляр Category) без родителя (свойство parent). На рис. 17.2 показан фрагмент такого дерева в базе данных. CATEGORY ID
NAME
PARENT_ID
1
One
2
Two
1
3
Three
1
4
Four
2
Рис. 17.2 Таблица базы данных, представляющая дерево категорий из примера
506 Настройка SQL-запросов Эти данные также можно представить графически, как показано на рис. 17.3. Также эти данные можно представить в виде последовательности путей с указанием уровня каждого узла: /One, 0 /One/Two, 1 /One/Three, 1 /One/Two/Four, 2
One(1, )
Two(2, 1)
Three(3, 1)
Four(4, 2)
Рис. 17.3 Дерево категорий
Теперь посмотрим, как приложение загружает экземпляры Category. Для этого необходимо найти корневую категорию (экземпляр Category). Это можно сделать с помощью простейшего запроса JPQL: select c from Category c where c.parent is null
Можно также с легкостью выбрать все категории на конкретном уровне дерева, например всех потомков корневой категории: select c from Category c, Category r where r.parent is null and c.parent = r
Этот запрос вернет только прямых потомков корневой категории: Two и Three. Но как в одном запросе загрузить дерево (или поддерево) целиком? Это невозможно сделать с помощью JPQL, поскольку потребовало бы рекурсии: «Загрузить все категории на данном уровне, затем всех потомков на следующем уровне, потом потомков потомков и т. д.». В SQL такой запрос можно написать, используя обобщенное табличное выражение (Common Table Expression, CTE), также известное как выделение подзапроса.
Загрузка дерева Следующий SQL-запрос, объявленный в XML-файле JPA, загрузит все дерево экземпляров Category: Файл: /model/src/main/resources/querying/NativeQueries.xml
Отображение результатов SQL-запросов 507 with CATEGORY_LINK(ID, NAME, PARENT_ID, PATH, LEVEL) as ( select ID, NAME, PARENT_ID, '/' || NAME, 0 from CATEGORY where PARENT_ID is null union all select c.ID, c.NAME, c.PARENT_ID, cl.PATH || '/' || c.NAME, cl.LEVEL + 1 from CATEGORY_LINK cl join CATEGORY c on cl.ID = c.PARENT_ID ) select ID, NAME as CAT_NAME, PARENT_ID, PATH, LEVEL from CATEGORY_LINK order by ID
Это сложный запрос, и мы не будем тратить на него много времени. Чтобы в нем разобраться, прочтите последнее выражение SELECT, выбирающее данные из представления CATEGORY_LINK. Каждая запись в этом представлении является узлом дерева. Представление определяется с помощью оператора WITH() AS. Представление CATEGORY_LINK объединяет результаты двух выражений SELECT. В процессе рекурсии добавляются дополнительные данные, такие как PATH (путь к узлу из корня) и LEVEL (уровень узла в дереве). Давайте отобразим результат этого запроса: Файл: /model/src/main/resources/querying/NativeQueries.xml
508 Настройка SQL-запросов Файл XML отображает атрибуты ID, CAT_NAME и PARENT_ID в свойства класса Category. Отображение вернет дополнительные скаляры PATH и LEVEL. Чтобы выполнить именованный запрос SQL, нужно написать следующий код: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/NativeQueries.java Query query = em.createNamedQuery("findAllCategories"); List result = query.getResultList(); for (Object[] tuple : result) { Category category = (Category) tuple[0]; String path = (String) tuple[1]; Integer level = (Integer) tuple[2]; // ... }
Каждый кортеж содержит управляемый хранимый экземпляр Category, абсолютный путь к нему от корня дерева (например, /One, /One/Two и т. д.) и уровень узла. Этот запрос SQL также можно объявить и отобразить в XML-файле метаданных Hibernate: Файл: /model/src/main/resources/querying/SQLQueries.hbm.xml
...
Мы опустили код SQL, поскольку он такой же, как в примере выше. Как упоминалось в разделе 14.4, в отношении выполнения кода Java не важно, где определяются именованные запросы – в файлах XML или в аннотациях. Даже язык не играет роли – это может быть как JPQL, так и SQL. Интерфейсы запросов в Hibernate и JPA обладают методами получения именованных запросов и их выполнения, независимо от места, где они определены. На этом мы заканчиваем рассматривать отображение результатов запросов SQL и переходим к исследованию настройки выражений SQL для операций CRUD, а также способов замены кода SQL, автоматически сгенерированного фреймворком Hibernate, для создания, чтения, изменения и удаления данных из базы.
Настройка операций CRUD 509 Особенности Hibernate
17.3. Настройка операций CRUD Запрос SQL в первом примере ниже загружает экземпляры сущности User. Во всех последующих примерах показан код SQL, который Hibernate выполняет автоматически, чтобы вы могли быстрее понять способы отображения. Настроить способ извлечения экземпляров сущности можно с помощью загрузчика.
17.3.1. Подключение собственных загрузчиков Переопределение запросов SQL для загрузки экземпляров сущности выполняется в два этапа: создать именованный запрос, извлекающий экземпляры сущности. Наш пример написан на SQL, но именованные запросы также можно писать на JPQL. В случае SQL может понадобиться дополнительное отображение результатов запроса, как было показано ранее в этой главе; активировать запрос, добавив перед классом сущности аннотацию @org.hibernate.annotations.Loader. После этого Hibernate будет использовать ваш запрос вместо сгенерированного по умолчанию. Давайте переопределим способ загрузки экземпляров User, как показано в лис тинге 17.2. Листинг 17.2 Загрузка экземпляров User с помощью произвольного запроса Файл: /model/src/main/java/org/jpwh/model/customsql/User.java @NamedNativeQueries({ Объявление запроса для загрузки экземпляров User @NamedNativeQuery( name = "findUserById", query = "select * from USERS where ID = ?", Символ подстановки resultClass = User.class Дополнительного отображения результатов не требуется ) }) @org.hibernate.annotations.Loader( Активация загрузчика с передачей имени запроса namedQuery = "findUserById" ) @Entity @Table(name = "USERS") public class User { // ... } Запрос для загрузки экземпляров User задается с помощью аннотаций; его также можно было бы определить в файле XML (с метаданными JPA или Hibernate). Этот запрос можно использовать для доступа к данным, когда потребуется.
510 Настройка SQL-запросов У запроса должен иметься только один символ подстановки для значения параметра, который Hibernate заменит значением идентификатора загружаемого экземпляра. Здесь используется позиционный параметр, но именованный тоже подойдет. Определять отображение результатов для такого простого запроса не нужно. Все поля, возвращаемые запросом, отображаются в классе User. Hibernate сможет автоматически преобразовать результат. После активации загрузчика для класса сущности с определением имени запроса этот запрос будет использоваться для всех операций загрузки экземпляров User из базы данных. Здесь нет и намека на язык запроса или способ его объявления; это не зависит от объявления загрузчика.
В именованном запросе загрузчика сущности необходимо выбрать (задать элементы проекции в предложении SELECT) следующие свойства класса сущности: свойство идентификатора или свойства, входящие в составной первичный ключ; скалярные свойства простых типов; все свойства, ссылающиеся на встроенные компоненты; идентификатор сущности для каждого свойства с аннотацией @JoinColum и отображаемой связью, такой как @ManyToOne, которой владеет данный класс; все скалярные значения, встроенные компоненты и ссылки для соединения по связи, находящиеся внутри аннотации @SecondaryTable; если для некоторых свойств настроить отложенную загрузку с помощью перехвата вызовов, вам не нужно будет загружать этих свойств (см. раздел 12.1.3). Hibernate всегда выполняет запрос активного загрузчика, когда требуется извлечь экземпляр User из базы данных. Например, ваш запрос будет выполняться при вызове em.find(User.class, USER_ID). Если выполнить цепочку вызовов some Item.getSeller().getUsername(), прокси-объект Item#seller будет инициализирован с помощью вашего запроса. Возможно, вам также потребуется настроить операции создания, изменения и удаления экземпляров User из базы данных.
17.3.2. Настройка операций создания, изменения, удаления Как правило, Hibernate генерирует SQL-код для операций CRUD во время запуска. Затем он кэширует выражения SQL для будущего использования, что позволяет во время выполнения не тратить времени на создание выражений SQL для наиболее типичных операций. Пока что вы знаете только, как переопределить букву R (read – чтение) из всей аббревиатуры CRUD, поэтому сейчас мы рассмотрим CUD (создание, изменение, удаление – create, update, delete). Для любой сущности можно определить произвольные SQL-выражения CUD, используя Hibernate-аннотации @SQLInsert, @SQLUpdate и @SQLDelete.
Настройка операций CRUD 511 Листинг 17.3 Замена DML-выражений для сущности User Файл: /model/src/main/java/org/jpwh/model/customsql/User.java @org.hibernate.annotations.SQLInsert( sql = "insert into USERS " + "(ACTIVATED, USERNAME, ID) values (?, ?, ?)" ) @org.hibernate.annotations.SQLUpdate( sql = "update USERS set " + "ACTIVATED = ?, " + "USERNAME = ? " + "where ID = ?" ) @org.hibernate.annotations.SQLDelete( sql = "delete from USERS where ID = ?" ) @Entity @Table(name = "USERS") public class User { // ... }
Будьте внимательны, связывая аргументы SQL-выражений с символами подстановки ?. Для операций CUD Hibernate поддерживает только позиционные параметры. Но какой порядок параметров правильный? Существует внутренний порядок связывания аргументов с параметрами SQL-выражений CUD. Чтобы узнать правильный порядок параметров в выражениях SQL, можно позволить Hibernate сгенерировать их примеры. Пока вы еще не добавили собственных выражений SQL, включите уровень журналирования DEBUG для категории org.hibernate.persister. entity, а затем, после запуска Hibernate, найдите в выводе программы все записи, похожие на следующие: Static SQL Insert 0: Update 0: Delete 0:
for entity: org.jpwh.model.customsql.User insert into USERS (activated, username, id) values (?, ?, ?) update USERS set activated=?, username=? where id=? delete from USERS where id=?
Эти автоматически сгенерированные выражения SQL подскажут верный порядок параметров, и Hibernate всегда будет выполнять связывание значений в таком же порядке. Скопируйте необходимые выражения SQL и поместите их в аннотации, внеся необходимые изменения. Особый случай представляют свойства класса сущности, отображаемые в другую таблицу с помощью аннотации @SecondaryTable. Настройка выражений CUD до сих пор затрагивала лишь столбцы основной таблицы сущности. Hibernate попрежнему будет выполнять автоматически сгенерированные выражения SQL для вставки, удаления и изменения строк во вторичной таблице (таблицах). Этот код SQL можно настроить, добавив перед классом сущности аннотацию @org.hiber-
512 Настройка SQL-запросов nate.annotations.Table и определив ее атрибуты sqlInsert, sqlUpdate и sqlDelete. При желании SQL-выражения CUD можно разместить в файле XML. В этом случае вам останется лишь описать всю сущность целиком в XML-файле метаданных Hibernate. Для произвольных выражений CUD используются элементы , и . К счастью, выражения CUD, как правило, гораздо проще запросов выборки, поэтому в большинстве приложений удобнее использовать аннотации. Теперь мы определили собственные SQL-выражения операций CRUD для экземпляра сущности. Пришла пора переопределить выражения SQL для загрузки и изменения коллекций.
17.3.3. Настройка операций над коллекциями Давайте переопределим выражения SQL, которые Hibernate использует для загрузки коллекции изображений Item#images. Это коллекция встраиваемых компонентов, отображаемая аннотацией @ElementCollection. К коллекциям базовых типов и связям типа @OneToMany или @ManyToMany применяется та же процедура. Листинг 17.4 Загрузка коллекции с помощью собственного запроса Файл: /model/src/main/java/org/jpwh/model/customsql/Item.java @Entity public class Item { @ElementCollection @org.hibernate.annotations.Loader(namedQuery = "loadImagesForItem") protected Set images = new HashSet(); }
// ...
Как и прежде, нужно объявить запрос для загрузки коллекции. Но на этот раз результаты запроса должны объявляться и отображаться в XML-файле метаданных Hibernate: это единственный способ, позволяющий отобразить результат запроса в свойство поля, представляющее коллекцию: Файл: /model/src/main/resources/customsql/ItemQueries.hbm.xml
select ITEM_ID, FILENAME, WIDTH, HEIGHT from ITEM_IMAGES where ITEM_ID = ?
Запрос должен иметь единственный (позиционный или именованный) параметр. Hibernate подставит вместо него значение идентификатора сущности, вла
Настройка операций CRUD 513 деющей коллекцией. При каждой инициализации коллекции Item#images Hibernate будет выполнять ваш запрос SQL. Но иногда переопределять весь запрос SQL для загрузки коллекции не требуется, например если нужно всего лишь добавить ограничение в сгенерированное выражение SQL. Предположим, что сущность Category содержит коллекцию объектов Item, и каждый объект Item имеет признак активности. Если свойство Item#active имеет значение false, при обходе коллекции Category#items такой объект загружать не нужно. Такое ограничение можно добавить в запрос SQL с помощью Hibernate-аннотации @Where, поместив ее перед отображением коллекции: Файл: /model/src/main/java/org/jpwh/model/customsql/Category.java @Entity public class Category { @OneToMany(mappedBy = "category") @org.hibernate.annotations.Where(clause = "ACTIVE = 'true'") protected Set items = new HashSet(); }
// ...
Как показано далее, также можно писать собственные запросы SQL для вставки и удаления элементов коллекции. Листинг 17.5 Произвольные выражения CUD для модификации коллекции Файл: /model/src/main/java/org/jpwh/model/customsql/Item.java @Entity public class Item { @ElementCollection @org.hibernate.annotations.SQLInsert( sql = "insert into ITEM_IMAGES " + "(ITEM_ID, FILENAME, HEIGHT, WIDTH) " + "values (?, ?, ?, ?)" ) @org.hibernate.annotations.SQLDelete( sql = "delete from ITEM_IMAGES " + "where ITEM_ID = ? and FILENAME = ? and HEIGHT = ? and WIDTH = ?" ) @org.hibernate.annotations.SQLDeleteAll( sql = "delete from ITEM_IMAGES where ITEM_ID = ?" ) protected Set images = new HashSet(); }
// ...
Чтобы определить правильный порядок параметров, включите уровень журналирования DEBUG для категории org.hibernate.persister.collection и найдите
514 Настройка SQL-запросов в журнале сгенерированные для этой коллекции выражения SQL; это нужно сделать перед тем, как добавлять аннотации SQL с собственными запросами. Ниже показана новая аннотация @SQLDeleteAll, которая может использоваться только для коллекций простых или встраиваемых типов. Hibernate выполнит это выражение SQL, когда потребуется удалить коллекцию из базы данных целиком: например, при вызове someItem .getImages().clear() или someItem.setImages(new HashSet()). Для этой коллекции не нужно использовать аннотацию @SQLUpdate, поскольку Hibernate не изменяет элементов коллекции встраиваемых типов. Когда меняется значение свойства изображения Image, для Hibernate это будет новый объект Image в коллекции (не забывайте, что изображения сравниваются «по значению», то есть сравниваются все их поля). Поэтому Hibernate выполнит операцию удаления DELETE для старого элемента и операцию вставки INSERT для нового. Вместо отложенной загрузки элементов коллекции их можно загружать немедленно, вместе с сущностью-владельцом. Также можно заменить этот запрос собственным выражением SQL.
17.3.4. Немедленное извлечение в собственном загрузчике Рассмотрим коллекцию ставок Item#bids и порядок ее загрузки. По умолчанию Hibernate использует отложенную загрузку, поскольку коллекция отображается аннотацией @OneToMany, и, следовательно, запрос для загрузки элементов выполнится, лишь когда начнется обход коллекции. Таким образом, во время загрузки экземпляра Item не придется загружать никаких элементов коллекции. Если, напротив, потребуется загрузить коллекцию Item#bids немедленно, вмес те с экземпляром Item, сначала нужно поместить аннотацию загрузчика с запросом перед определением класса Item: Файл: /model/src/main/java/org/jpwh/model/customsql/Item.java @org.hibernate.annotations.Loader( namedQuery = "findItemByIdFetchBids" ) @Entity public class Item { @OneToMany(mappedBy = "item") protected Set bids = new HashSet(); }
// ...
Как и в предыдущих примерах, необходимо определить именованный запрос в XML-файле метаданных Hibernate, поскольку не существует аннотаций для загрузки коллекций с помощью именованных запросов. Ниже показан код SQL, загружающий экземпляр Item вместе с коллекцией bids с помощью внешнего соединения (OUTER JOIN):
Настройка операций CRUD 515 Файл: /model/src/main/resources/customsql/ItemQueries.hbm.xml
select {i.*}, {b.*} from ITEM i left outer join BID b on i.ID = b.ITEM_ID where i.ID = ?
Вы уже видели этот запрос с отображением результатов в коде Java выше в этой главе, в разделе «Немедленное извлечение коллекций». Здесь добавлено ограничение, допускающее возврат единственной записи из таблицы ITEM с заданным первичным ключом. Аналогично можно немедленно загружать связи с единственным значением, такие как @ManyToOne, используя собственные запросы SQL. Предположим, что требуется немедленно загрузить свойство bidder вместе с экземпляром Bid. Для начала нужно определить загрузчик с именованным запросом: Файл: /model/src/main/java/org/jpwh/model/customsql/Bid.java @org.hibernate.annotations.Loader( namedQuery = "findBidByIdFetchBidder" ) @Entity public class Bid { @ManyToOne(optional = false, fetch = FetchType.LAZY) protected User bidder; }
// ...
В отличие от запросов, загружающих коллекции, свой именованный запрос можно определить с помощью стандартных аннотаций (конечно, можно помес тить его в файл XML, используя синтаксис JPA или Hibernate): Файл: /model/src/main/java/org/jpwh/model/customsql/Bid.java @NamedNativeQueries({ @NamedNativeQuery( name = "findBidByIdFetchBidder", query = "select " + "b.ID as BID_ID, b.AMOUNT, b.ITEM_ID, b.BIDDER_ID, " + "u.ID as USER_ID, u.USERNAME, u.ACTIVATED " +
516 Настройка SQL-запросов "from BID b join USERS u on b.BIDDER_ID = u.ID " + "where b.ID = ?", resultSetMapping = "BidBidderResult"
) }) @Entity public class Bid { // ... }
Внутреннее соединение (INNER JOIN) вполне уместно в данном запросе SQL, поскольку свойство bidder экземпляра Bid не может быть пустым, а столбец внешнего ключа BIDDER_ID не может принимать значения NULL. Поскольку в запросе нужно переименовывать повторяющиеся столбцы ID в BID_ID и USER_ID, понадобится собственное отображение результата запроса: Файл: /model/src/main/java/org/jpwh/model/customsql/Bid.java @SqlResultSetMappings({ @SqlResultSetMapping( name = "BidBidderResult", entities = { @EntityResult( entityClass = Bid.class, fields = { @FieldResult(name = "id", column = "BID_ID"), @FieldResult(name = "amount", column = "AMOUNT"), @FieldResult(name = "item", column = "ITEM_ID"), @FieldResult(name = "bidder", column = "BIDDER_ID") } ), @EntityResult( entityClass = User.class, fields = { @FieldResult(name = "id", column = "USER_ID"), @FieldResult(name = "username", column = "USERNAME"), @FieldResult(name = "activated", column = "ACTIVATED") } ) } ) }) @Entity public class Bid { // ... }
Hibernate выполнит этот запрос SQL и отобразит результаты во время загрузки экземпляра Bid при вызове метода em.find(Bid.class, BID_ID) или при инициализа-
Вызов хранимых процедур 517 ции прокси-объекта Bid. Hibernate сразу же загрузит поле Bid#bidder, невзирая на параметр FetchType.LAZY в параметрах связи. Вот мы и настроили выполнение собственных запросов SQL для всех операций. Далее мы рассмотрим хранимые процедуры и узнаем, как интегрировать их в приложение Hibernate.
17.4. Вызов хранимых процедур Хранимые процедуры часто используются при разработке приложений баз данных. Размещение кода ближе к данным, и его выполнение внутри базы данных дает определенные преимущества. Это позволяет избежать дублирования функциональности и логики во всех программах, работающих с этими данными. Также принято считать, что общая бизнес-логика тоже не должна дублироваться всеми приложениями. К такой логике относятся процедуры, обеспечивающие целостность данных, проверяющие ограничения, которые слишком сложно описывать декларативно. Вы часто будете встречать в базах данных триггеры, проверяющие правила целостности. Хранимые процедуры показывают свое преимущество при обработке больших объемов данных для создания отчетов или статистического анализа. Вы должны стараться избегать передачи больших объемов данных между базой и сервером приложения, поэтому при обработке больших объемов следует в первую очередь использовать хранимые процедуры. Конечно, существуют системы (часто унаследованные), которые даже базовые операции CRUD реализуют с помощью хранимых процедур. Стоит упомянуть и о системах, не позволяющих напрямую выполнять SQL-выражения INSERT, UPDATE или DELETE, а допускающих только вызовы хранимых процедур; когда-то такие системы применялись очень широко (и даже сейчас иногда применяются). Некоторые СУБД позволяют объявлять пользовательские функции вместо или вместе с хранимыми процедурами. В табл. 17.1 перечислены некоторые различия между процедурами и функциями. Таблица 17.1. Сравнение процедур и функций, определяемых в базе данных Хранимая процедура Может иметь входные и/или выходные параметры Возвращет ноль, одно или несколько значений Может вызываться только с помощью JDBC-объекта CallableStatement
Функция Может иметь входные параметры Должна возвращать значение (хотя оно не обязательно будет скаляром или даже NULL) Может вызываться прямо в предложениях SELECT, WHERE и т. д.
Трудно обобщать и сравнивать процедуры и функции вне этих очевидных различий. Разные СУБД по-разному их поддерживают: некоторые не поддерживают хранимых процедур или функций, определяемых пользователем, тогда как другие не делают между ними различий (так, например, в PostgreSQL есть только
518 Настройка SQL-запросов пользовательские функции). Языки программирования для описания хранимых процедур обычно имеют специфические особенности для каждой СУБД. Некоторые базы данных даже поддерживают хранимые процедуры, написанные на языке Java. Стандартизация хранимых процедур на Java проводилась в рамках стандарта SQLJ, который, к сожалению, не возымел успеха. В этом разделе мы покажем, как интегрировать хранимые процедуры MySQL и пользовательские функции PostgreSQL с Hibernate. Сначала мы рассмотрим объявления и вызовы хранимых процедур с помощью стандартного Java Persistence API и оригинального Hibernate API. Затем настроим и заменим CRUD-операции Hibernate вызовами хранимых процедур. Важно, чтобы вы прочитали предыдущий раздел перед этим, поскольку интеграция хранимых процедур основана на тех же способах отображения, что и остальные модификации выражений SQL в Hibernate. Как и ранее в этой главе, хранимые процедуры SQL в примерах довольно прос ты, чтобы вы могли сосредоточиться на более важных аспектах: вызове процедур и использовании API в своем приложении. Вызывая хранимую процедуру, вы обычно передаете входные аргументы и получаете возвращаемое значение. Процедуры можно разделить на следующие категории: возвращают результат запроса; возвращают несколько результатов запросов; изменяют данные и возвращают количество измененных строк; принимают входные и/или выходные аргументы; возвращают курсор, ссылающийся на результат в базе данных. Рассмотрим простейший случай: хранимую процедуру без параметров, которая возвращает только результат запроса.
17.4.1. Возврат результата запроса В MySQL можно создать следующую процедуру. Она возвращает результат запроса, содержащий все строки таблицы ITEM: Файл: /model/src/main/resources/querying/StoredProcedures.hbm.xml create procedure FIND_ITEMS() begin select * from ITEM; end
Чтобы вызвать ее, с помощью объекта EntityManager нужно создать экземпляр запроса StoredProcedureQuery и выполнить его: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ CallStoredProcedures.java StoredProcedureQuery query = em.createStoredProcedureQuery( "FIND_ITEMS", Item.class Или имя отображения результатов запроса );
Вызов хранимых процедур 519 List result = query.getResultList(); for (Item item : result) { // ... }
Как вы уже видели ранее в этой главе, Hibernate автоматически отобразит столбцы возвращаемого результата в свойства класса Item. Экземпляры Item, возвращаемые этим запросом, будут находиться в управляемом хранимом состоянии. Вместо параметра Item.class методу можно передать имя отображения. Особенности Hibernate Используя оригинальный интерфейс Session фреймворка Hibernate, можно получить результат вызова хранимой процедуры с помощью объекта Procedure. Файл: /example/src/test/java/org/jpwh/test/querying/sql org.hibernate.procedure.ProcedureCall call = session.createStoredProcedureCall("FIND_ITEMS", Item.class); org.hibernate.result.ResultSetOutput resultSetOutput = (org.hibernate.result.ResultSetOutput) call.getOutputs().getCurrent(); List result = resultSetOutput.getResultList();
Метод getCurrent() как бы намекает, что процедура может возвращать более одного объекта ResultSet. Процедура может вернуть не только несколько результатов, но также количество произведенных изменений, если она изменяла какието данные.
17.4.2. Возврат нескольких результатов и количества изменений Следующая процедура MySQL возвращает все записи из таблицы ITEM, которые не были одобрены, меняя их статус APPROVED, а также все записи, которые были одобрены: Файл: /model/src/main/resources/querying/StoredProcedures.hbm.xml create procedure APPROVE_ITEMS() begin select * from ITEM where APPROVED = 0; select * from ITEM where APPROVED = 1; update ITEM set APPROVED = 1 where APPROVED = 0; end
Приложение в этом случае получит два результата запроса и количество произведенных изменений: доступ к результатам вызова процедуры и их обработка выглядят не так просто, но поскольку JPA тесно связан с JDBC, то, если вы уже работали с хранимыми процедурами, следующий код будет вам знаком:
520 Настройка SQL-запросов Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ CallStoredProcedures.java StoredProcedureQuery query = em.createStoredProcedureQuery( "APPROVE_ITEMS", Item.class Или имя отображения результатов запроса ); boolean isCurrentReturnResultSet = query.execute(); Вызов метода execute() while (true) { Обработка результатов вызова if (isCurrentReturnResultSet) { Обработка результатов запроса List result = query.getResultList(); // ... } else { int updateCount = query.getUpdateCount(); Счетчики изменений закончились: if (updateCount > -1) { выход из цикла // ... } else { break; Обработка счетчиков изменений } } }
isCurrentReturnResultSet = query.hasMoreResults();
Переход к следующему результату
Вызов хранимой процедуры методом execute(). Он вернет true, если хранимая процедура вернула результат запроса, и false, если результатом является счетчик изменений. Обработка всех результатов в цикле. Когда результатов не останется, происходит выход из цикла: в этом случае метод hasMoreResults() вернет false, а метод getUpdateCount() вернет –1. Если процедура вернула результат запроса, обработать его. Hibernate отобразит столбцы каждого результата в управляемые экземпляры класса Item. Также можно использовать собственное отображение результатов, применимое для всех результатов, возвращаемых процедурой. Если текущее возвращаемое значение является счетчиком изменений, метод getUpdateCount() вернет значение больше –1. Метод hasMoreResults() выполнит переход к следующему результату и укажет его тип.
Особенности Hibernate Вызов хранимых процедур с помощью Hibernate может показаться более простым. Фреймворк скрывает сложности, связанные с проверкой типа каждого результата и наличия оставшихся выходных значений процедуры: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ CallStoredProcedures.java org.hibernate.procedure.ProcedureCall call = session.createStoredProcedureCall("APPROVE_ITEMS", Item.class); org.hibernate.procedure.ProcedureOutputs callOutputs = call.getOutputs();
Вызов хранимых процедур 521 org.hibernate.result.Output output; Проверяет наличие while ((output = callOutputs.getCurrent()) != null) { выходных значений if (output.isResultSet()) { Выходное значение – результат запроса? List result = ((org.hibernate.result.ResultSetOutput) output) .getResultList(); // ... } else { int updateCount = Выходное значение является счетчиком обновлений ((org.hibernate.result.UpdateCountOutput) output) .getUpdateCount(); // ... } if (!callOutputs.goToNext()) Продолжить break; } Пока метод getCurrent() не вернул null, имеются выходные значения для обработки. Выходное значение может быть результатом запроса: проверить это и выполнить приведение типа. Если выходное значение не является результатом запроса, это счетчик обновлений. Если остались еще выходные значения, продолжить обработку.
Далее мы рассмотрим хранимые процедуры с входными и выходными парамет рами.
17.4.3. Передача входных и выходных аргументов Следующая хранимая процедура MySQL вернет запись из таблицы ITEM с заданным идентификатором, а также количество записей в таблице: Файл: /model/src/main/resources/querying/StoredProcedures.hbm.xml create procedure FIND_ITEM_TOTAL(in PARAM_ITEM_ID bigint, out PARAM_TOTAL bigint) begin select count(*) into PARAM_TOTAL from ITEM; select * from ITEM where ID = PARAM_ITEM_ID; end
Следующая процедура вернет результат запроса с данными из записи ITEM. Дополнительно ей передается выходной параметр PARAM_TOTAL. Для вызова хранимой процедуры с помощью JPA сначала нужно описать все параметры. Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ CallStoredProcedures.java StoredProcedureQuery query = em.createStoredProcedureQuery( "FIND_ITEM_TOTAL", Item.class
522 Настройка SQL-запросов );
Описание параметров
query.registerStoredProcedureParameter(1, Long.class, ParameterMode.IN); query.registerStoredProcedureParameter(2, Long.class, ParameterMode.OUT); query.setParameter(1, ITEM_ID);
Связывание значений параметров
List result = query.getResultList(); for (Item item : result) { // ... }
Получение результатов
Long totalNumberOfItems = (Long) query.getOutputParameterValue(2);
Получение значений выходных параметров
Описание параметров и их типов с их порядковыми номерами (начиная с 1). Фактические значения связываются с входными параметрами. Извлекается результат запроса, возвращаемый хранимой процедурой. После извлечения результата можно прочитать значения выходных параметров.
Также можно описывать и использовать именованные параметры, но позиционные и именованные параметры нельзя смешивать в одном вызове. Обратите еще внимание, что имена параметров в коде Java не обязательно должны совпадать с именами параметров хранимой процедуры. Проще говоря, вы должны описать параметры в том же порядке, как и в определении хранимой процедуры. Особенности Hibernate Оригинальный интерфейс Hibernate упрощает описание и использование параметров: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ CallStoredProcedures.java org.hibernate.procedure.ProcedureCall call = session.createStoredProcedureCall("FIND_ITEM_TOTAL", Item.class); call.registerParameter(1, Long.class, ParameterMode.IN) .bindValue(ITEM_ID);
Описание параметров
Получение объекта описания параметра ParameterRegistration totalParameter = call.registerParameter(2, Long.class, ParameterMode.OUT);
org.hibernate.procedure.ProcedureOutputs callOutputs = call.getOutputs(); Обработка результатов запроса org.hibernate.result.Output output; while ((output = callOutputs.getCurrent()) != null) { if (output.isResultSet()) { org.hibernate.result.ResultSetOutput resultSetOutput = (org.hibernate.result.ResultSetOutput) output; List result = resultSetOutput.getResultList(); for (Item item : result) { // ... }
Вызов хранимых процедур 523
}
} if (!callOutputs.goToNext()) break;
Long totalNumberOfItems = callOutputs.getOutputParameterValue(totalParameter);
Получение значений выходых параметров
Описание всех параметров; можно сразу же выполнить связывание значений. Описание выходных параметров можно повторно использовать при получении их значений. Перед получением значений выходных параметров нужно обработать все возвращаемые результаты. Получение значения выходного параметра с использованием его описания.
Следующая процедура MySQL использует входные параметры для изменения названия товара в записи, в таблице ITEM: Файл: /model/src/main/resources/querying/StoredProcedures.hbm.xml create procedure UPDATE_ITEM(in PARAM_ITEM_ID bigint, in PARAM_NAME varchar(255)) begin update ITEM set NAME = PARAM_NAME where ID = PARAM_ITEM_ID; end
Эта процедура не возвращает результатов запроса – только счетчик изменений, поэтому вызвать ее довольно просто: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ CallStoredProcedures.java StoredProcedureQuery query = em.createStoredProcedureQuery( "UPDATE_ITEM" ); query.registerStoredProcedureParameter("itemId", Long.class, ParameterMode.IN); query.registerStoredProcedureParameter("name", String.class, ParameterMode.IN); query.setParameter("itemId", ITEM_ID); query.setParameter("name", "New Item Name"); assertEquals(query.executeUpdate(), 1);
Счетчик изменений равен 1
// Альтернативный вариант: // assertFalse(query.execute()); // assertEquals(query.getUpdateCount(), 1);
Первое возвращаемое значение– НЕ результат запроса
В этом примере также можно видеть, как работать с именованными параметрами и что имена в коде Java не обязаны совпадать с именами из объявления хранимой процедуры. Но порядок описания параметров по-прежнему важен: PARAM_ ITEM_ID должен следовать первым, PARAM_ITEM_NAME – вторым.
524 Настройка SQL-запросов Особенности Hibernate Если вызываемая процедура не возвращает результатов, а лишь изменяет данные, ее вызов можно упростить, использовав метод executeUpdate(), который возвращает только счетчик изменений. Также можно последовательно вызвать методы execute() и getUpdateCount(). Ниже показан вызов той же процедуры с помощью Hibernate: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ CallStoredProcedures.java org.hibernate.procedure.ProcedureCall call = session.createStoredProcedureCall("UPDATE_ITEM"); call.registerParameter(1, Long.class, ParameterMode.IN) .bindValue(ITEM_ID); call.registerParameter(2, String.class, ParameterMode.IN) .bindValue("New Item Name"); org.hibernate.result.UpdateCountOutput updateCountOutput = (org.hibernate.result.UpdateCountOutput) call.getOutputs().getCurrent(); assertEquals(updateCountOutput.getUpdateCount(), 1);
Поскольку известно, что процедура не возвращает результатов, можно сразу выполнить приведение первого (текущего) выходного значения к типу UpdateCountOutput. Далее мы рассмотрим случай, когда вместо результата запроса процедура возвращает ссылку на курсор.
17.4.4. Возвращение курсора В MySQL хранимая процедура не может вернуть курсор. Следующий пример будет работать только с PostgreSQL. Следующая хранимая процедура (или пользовательская функция, поскольку в PostgreSQL – это одно и то же) возвращает курсор для обхода всех записей в таблице ITEM: Файл: /model/src/main/resources/querying/StoredProcedures.hbm.xml create function FIND_ITEMS() returns refcursor as $$ declare someCursor refcursor; begin open someCursor for select * from ITEM; return someCursor; end; $$ language plpgsql;
В JPA курсоры описываются как параметры с помощью специального значения ParameterMode.REF_CURSOR:
Вызов хранимых процедур 525 Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ CallStoredProcedures.java StoredProcedureQuery query = em.createStoredProcedureQuery( "FIND_ITEMS", Item.class ); query.registerStoredProcedureParameter( 1, void.class, ParameterMode.REF_CURSOR ); List result = query.getResultList(); for (Item item : result) { // ... }
Параметр имеет тип void, поскольку его единственная цель состоит в подготовке вызова для дальнейшего чтения данных с помощью курсора. Когда будет вызван метод getResultList(), Hibernate сможет вернуть необходимый результат. Особенности Hibernate Hibernate также поддерживает автоматическую работу с курсорами: Файл: /examples/src/test/java/org/jpwh/test/querying/sql/ CallStoredProcedures.java org.hibernate.procedure.ProcedureCall call = session.createStoredProcedureCall("FIND_ITEMS", Item.class); call.registerParameter(1, void.class, ParameterMode.REF_CURSOR); org.hibernate.result.ResultSetOutput resultSetOutput = (org.hibernate.result.ResultSetOutput) call.getOutputs().getCurrent(); List result = resultSetOutput.getResultList(); for (Item item : result) { // ... }
Обход результатов запроса с помощью курсора В разделе 14.3.3 мы обсуждали использование курсоров базы данных для обхода потенциально объемных результатов запроса. К сожалению, на момент создания этой книги данная функциональность не поддерживалась ни в JPA, ни в Hibernate. Hibernate будет всегда извлекать в память целиком все результаты запроса, на которые ссылается курсор.
526 Настройка SQL-запросов Поддерживать работу с курсорами в различных диалектах СУБД довольно сложно, поэтому Hibernate имеет некоторые ограничения. Например, в PostgreSQL параметр, указывающий на курсор, всегда должен объявляться первым, и (поскольку речь идет о функции) получить из базы данных можно только один курсор. При работе с курсорами в диалекте PostgreSQL Hibernate не поддерживает именованных параметров: вместо них следует использовать позиционные. За более подробной информацией обращайтесь к описанию SQL-диалекта своей базы данных в Hibernate: обратите внимание на такие методы, как Dialect#getRe sultSet(CallableStatement) и т. д. На этом мы заканчиваем рассмотрение API для вызова хранимых процедур. Далее мы воспользуемся хранимыми процедурами для переопределения сгенерированных выражений, которые Hibernate выполняет для загрузки и сохранения данных. Особенности Hibernate
17.5. Применение хранимых процедур для операций CRUD Первой операцией CRUD, которую мы настроим, будет загрузка экземпляров сущностей класса User. Ранее в этой главе вы использовали для этого обычный запрос SQL и загрузчик. Если для загрузки экземпляра User требуется вызвать хранимую процедуру, сделать это так же просто.
17.5.1. Загрузчик, вызывающий процедуру Прежде всего нужно создать именованный запрос, вызывающий хранимую процедуру, например с помощью аннотации перед классом User: Файл: /model/src/main/java/org/jpwh/model/customsql/procedures/User.java @NamedNativeQueries({ @NamedNativeQuery( name = "findUserById", query = "{call FIND_USER_BY_ID(?)}", resultClass = User.class ) }) @org.hibernate.annotations.Loader( namedQuery = "findUserById" ) @Entity @Table(name = "USERS") public class User { // ... }
Применение хранимых процедур для операций CRUD 527 Сравните его с предыдущей версией в разделе 17.3.1: объявление загрузчика осталось прежним, и ему нужен лишь именованный запрос на любом поддерживаемом языке. Изменился только сам запрос, который также можно поместить в XML-файл метаданных для еще большего разделения функциональности. JPA не накладывает ограничений на содержимое аннотации @NamedNativeQuery: вы можете написать любое выражение SQL. Используя синтаксис экранирования JDBC с фигурными скобками, вы как бы говорите: «Пусть драйвер JDBC сам решает, что с этим делать». Если ваш драйвер JDBC и СУБД умеют работать с хранимыми процедурами, процедуру можно вызвать с помощью конструкции {call PROCEDURE}. Hibernate ожидает, что процедура вернет результат запроса, а первая запись в нем будет содержать все столбцы, необходимые для создания экземпляра User. Все эти столбцы и свойства были перечислены в разделе 17.3.1. Не забывайте, что всегда можно использовать собственное отображение результатов запроса, если столбцы (их имена), возвращаемые процедурой, не вполне вас устраивают, или когда невозможно изменить код процедуры. Хранимая процедура должна иметь сигнатуру, позволяющую сделать вызов с единственным аргументом. Hibernate будет использовать этот аргумент как идентификатор при загрузке экземпляра User. Ниже показана хранимая процедура в MySQL, обладающая такой сигнатурой: Файл: /model/src/main/resources/customsql/CRUDProcedures.hbm.xml create procedure FIND_USER_BY_ID(in PARAM_USER_ID bigint) begin select * from USERS where ID = PARAM_USER_ID; end
Далее мы воспользуемся хранимыми процедурами для создания, изменения и удаления объекта User.
17.5.2. Использование процедур в операциях CUD Для настройки запросов, которые Hibernate использует с целью создания, изменения и удаления экземпляров сущности из базы данных, используются аннотации @SQLInsert, @SQLUpdate, and @SQLDelete. Вместо обычных выражений SQL для этих операций также можно вызывать хранимые процедуры: Файл: /model/src/main/java/org/jpwh/model/customsql/procedures/User.java @org.hibernate.annotations.SQLInsert( sql = "{call INSERT_USER(?, ?, ?)}", callable = true ) @org.hibernate.annotations.SQLUpdate( sql = "{call UPDATE_USER(?, ?, ?)}", callable = true, check = ResultCheckStyle.NONE
528 Настройка SQL-запросов ) @org.hibernate.annotations.SQLDelete( sql = "{call DELETE_USER(?)}", callable = true ) @Entity @Table(name = "USERS") public class User { // ... }
Чтобы Hibernate использовал объект JDBC, представляющий вызов процеду ры – CallableStatement вместо PreparedStatement, – нужно указать параметр callable=true. Как объяснялось в разделе 17.3.2, для связывания аргументов в вызове процедуры можно использовать только позиционные параметры, и объявлены они должны быть в том порядке, в каком Hibernate ожидает их увидеть. Хранимые процедуры должны иметь соответствующую сигнатуру. Ниже показано несколько процедур MySQL, которые вставляют, изменяют и удаляют записи из таблицы USERS: Файл: /model/src/main/resources/customsql/CRUDProcedures.hbm.xml create procedure INSERT_USER(in PARAM_ACTIVATED bit, in PARAM_USERNAME varchar(255), in PARAM_ID bigint) begin insert into USERS (ACTIVATED, USERNAME, ID) values (PARAM_ACTIVATED, PARAM_USERNAME, PARAM_ID); end Файл: /model/src/main/resources/customsql/CRUDProcedures.hbm.xml create procedure UPDATE_USER(in PARAM_ACTIVATED bit, in PARAM_USERNAME varchar(255), in PARAM_ID bigint) begin update USERS set ACTIVATED = PARAM_ACTIVATED, USERNAME = PARAM_USERNAME where ID = PARAM_ID; end Файл: /model/src/main/resources/customsql/CRUDProcedures.hbm.xml create procedure DELETE_USER(in PARAM_ID bigint) begin delete from USERS where ID = PARAM_ID; end
После того как хранимая процедура вставит, удалит или изменит экземпляр User, Hibernate должен узнать результат ее выполнения. В динамически генери-
Резюме 529 руемых SQL-запросах Hibernate проверяет количество измененных строк после операции. Если включено версионирование (см. раздел 11.2.2) и выполняемая операция не смогла обновить ни одной записи, произойдет отказ оптимистической блокировки. Вызывая собственный код SQL, можно это поведение настроить. Хранимая процедура будет сама решать, требуется ли сравнивать текущую версию с версией из базы данных для операций изменения или удаления. Используя параметр аннотации check, вы сообщаете Hibernate, как процедура должна реа лизовать это требование. По умолчанию используется значение ResultCheckStyle.NONE, но также доступны следующие варианты: NONE – процедура возбудит исключение, если операция завершится неудачей. Hibernate не будет выполнять никаких проверок и полностью положится в этом на процедуру. Если включено версионирование, ваша процедура должна сравнить/увеличить номер версии, а при обнаружении отличий – возбудить исключение; COUNT – процедура увеличит номер версии, проверит и вернет число измененных записей. Для получения счетчика изменений Hibernate вызовет Cal lableStatement#getUpdateCount(); PARAM – процедура выполнит увеличение номера версии и проверку, а затем вернет количество измененных записей в первом выходном параметре. В этом случае нужно добавить дополнительный знак вопроса в сигнатуру хранимой процедуры и возвращать число изменившихся записей в этом (первом) выходном параметре. Hibernate автоматически зарегистрирует этот параметр и прочитает его значение после вызова процедуры. Поддержка параметров ResultCheckStyle На момент написания книги Hibernate поддерживал только параметр ResultCheckStyle.NONE.
И наконец, не забывайте, что Hibernate не всегда может взаимодействовать с хранимыми процедурами и функциями. В таких случаях следует использовать обычный JDBC. Иногда вызов унаследованной хранимой процедуры можно обернуть вызовом новой хранимой процедуры, интерфейс которой будет соответствовать требованиям Hibernate.
17.6. Резюме Вы узнали, как можно использовать знакомый JDBC API. Даже при использовании произвольных SQL-запросов Hibernate может взять всю тяжелую работу на себя и преобразовать коллекцию результатов ResultSet в экземпляры классов модели предметной области, предоставляя гибкие настройки, включая возможность применения собственного отображения результатов запроса. Для упрощения настроек запросы можно определять во внешних файлах.
530 Настройка SQL-запросов Мы рассмотрели возможности переопределения SQL-выражений для стандартных операций создания, чтения, изменения и удаления (CRUD), а также для операций над коллекциями. Вы можете определять собственные загрузчики и использовать в них немедленное извлечение. Вы узнали, как вызывать процедуры, хранимые в базе данных, и интегрировать их с Hibernate. Вы узнали, как обрабатывать один или несколько результатов запроса, а также счетчик изменений. Вы узнали, как передаются параметры в хранимые процедуры (входные и выходные) и как они возвращают курсоры базы данных. Также вы узнали, как можно использовать хранимые процедуры для операций CRUD.
Часть
V ils gDeta Billin ing tr S r: owne
СОЗДАНИЕ ПРИЛОЖЕНИЙ > LA ST NAM > > ME
d itCar Cred ng ing id : Lo mber : Str u cardN nth : String o expM r : String a expYe
unt Acco Bank g n id : Lo t : String n g accou me : Strin a bankn tring :S swift
В пятой и последней части в этой книге мы обсудим проектирование и реализа K ESS > ZIP REтакие > используемые с Hibernate, как объект доступа к данным (Data Access Object, E CI CO T > DE DAO). Вы увидите, какTYможно легко протестировать приложение, использующее Hibernate, и какие примы лучше использовать при работе с программным обеспечением для объектно-реляционного отображения (ORM) в веб-приложениях или клиент-серверных приложениях в целом. Глава 18 целиком посвящена созданию клиент-серверных приложений. Вы познакомитесь с шаблонами клиент-серверной архитектуры, создадите и протестируете уровень хранения, а затем интегрируете экземпляры EJB с JPA. В главе 19 вы изучите создание веб-приложений и способы интеграции JPA с CDI и JSF. Научитесь просматривать данные в таблицах, осуществлять продолжительные диалоговые взаимодействия и настраивать сериализацию сущностей. Наконец, в главе 20 мы покажем возможности масштабирования Hibernate с применением массовых операций и разделяемого кэша. После прочтения этой части вы получите все необходимые знания по архитектуре, которые позволят вам не только создавать приложения, но и успешно их масштабировать.
ble > ID 0 ? getBidsHighestFirst().get(0) : null; } public List getBidsHighestFirst() { List list = new ArrayList(getBids()); Collections.sort(list); return list; } }
// ...
Метод isValid() выполняет несколько проверок, чтобы выяснить, превосходит ли значение текущего объекта Bid последнюю ставку. Если однажды понадобится реализовать в аукционной системе стратегию «побеждает наименьшая ставка»,
550 Проектирование клиент-серверных приложений вам достаточно будет поменять реализацию класса Item в предметной модели; для служб и интерфейсов DAO, использующих класс, никакой разницы не будет. (Очевидно, вам понадобится вывести другое сообщение для исключения InvalidBidException.) Сомнения вызывает лишь эффективность метода getHighestBid(). Он загружает всю коллекцию bids в память, сортирует ее, а затем выбирает один экземпляр Bid. Улучшенный вариант мог бы выглядеть так: Файл: /apps/app-model/src/main/java/org/jpwh/model/Item.java @Entity public class Item implements Serializable { // ... public boolean isValidBid(Bid newBid, Bid currentHighestBid, Bid currentLowestBid) { // ... } }
Служба (или, если угодно, контроллер) по-прежнему ничего не знает о бизнеслогике; ей не нужно знать, должна ли новая ставка быть больше или меньше предыдущей. Реализация службы должна передать значения наибольшей и наименьшей ставок, currentHighestBid и currentLowestBid, при вызове Item#isValid(). Как раз на это мы и намекали ранее, когда говорили, что может понадобиться добавить операции в класс BidDAO. Чтобы получить эти ставки самым эффективным способом, без загрузки всей коллекции в память и последующей сортировки, можно использовать запросы. Теперь приложение готово. Оно реализует два запланированных варианта использования. Давайте сделаем шаг назад и проанализируем результаты.
18.2.3. Анализ приложения без состояния Мы реализовали диалоговые взаимодействия, каждое из которых с точки зрения пользователя представляет единицу работы. Пользователь ожидает выполнить ряд шагов, изменения в которых будут временными лишь до тех пор, пока не произойдет их подтверждение на этапе, завершающем диалоговое взаимодействие. Обычно последний шаг – это завершающий запрос, посылаемый клиентом серверу. Это очень похоже на описание транзакции, но вам, возможно, придется создать несколько системных транзакций на сервере для завершения конкретного диалогового взаимодействия. Проблема заключается в том, как добиться атомарности для нескольких запросов и системных транзакций. Диалоговые взаимодействия могут иметь любую продолжительность и сложность. В процессе диалогового взаимодействия отсоединенные данные могут быть загружены более чем одним клиентским запросом. Поскольку отсоединенные сущности на стороне клиента находятся под вашим контролем, вы легко можете сде-
Создание сервера без состояния 551 лать диалоговое взаимодействие атомарным, если не будете выполнять слияния, сохранения или удаления на сервере, пока не получите завершающего запроса. Вам решать, как накапливать список изменений и где хранить отсоединенные данные, пока пользователь принимает решение. Просто не вызывайте со стороны клиента никаких служебных операций, сохраняющих изменения на сервере, пока не будете уверены, что готовы «подтвердить» (commit) диалоговое взаимодействие. Одним из важных вопросов, требующих внимания, является сравнение отсоединенных экземпляров: например, если понадобится загрузить несколько экземпляров Item и поместить их во множество Set или использовать в качестве ключей словаря Map. Поскольку экземпляры будут сравниваться вне области гарантированной идентичности объектов – контекста хранения, – необходимо переопределить методы equals() и hashCode() класса сущности Item, как было показано в разделе 10.3.1. В простейшем диалоговом взаимодействии, где использовался только список отсоединенных экземпляров Item, это было не нужно. Мы не сравнивали экземпляров во множестве Set, не использовали в качестве ключей в словаре HashMap, не проверяли их явно на равенство. Вы должны использовать версионирование сущности Item для работы в многопользовательском приложении, как объяснялось в разделе «Включаем версионирование» в главе 11. При слиянии изменений в методе AuctionService#storeItem() Hibernate автоматически увеличит версию экземпляра Item (только если экземпляр Item был изменен). Следовательно, если несколько пользователей одновременно изменят название товара Item, Hibernate возбудит исключение во время подтверждения системной транзакции и выталкивания контекста хранения. При выборе оптимистичной стратегии побеждает пользователь, который первым подтвердит изменения, сделанные в ходе диалогового взаимодействия. Второй пользователь должен увидеть обычное сообщение: «Извините, кто-то уже изменил эти данные; пожалуйста, начните сначала». Мы только что реализовали систему с толстым клиентом (rich client); толстый клиент – это не просто терминал ввода/вывода, а приложение со своим внутренним состоянием, независимым от сервера (вспомните, что сервер не хранит никакого состояния). Одним из преимуществ такого сервера без состояния является возможность обработки запроса пользователя любым сервером. Если на сервере произойдет сбой, можно перенаправить запрос на другой сервер, и диалоговое взаимодействие продолжится. У серверов в кластере нет ничего общего; вы можете с легкостью масштабировать систему горизонтально, подключая больше серверов. Очевидно, что все серверы приложений обращаются к общей базе данных, но вам придется побеспокоиться о масштабировании только одного уровня серверов. Сохранение изменений после выхода из состояния гонки Во время приемочного тестирования может обнаружиться, что пользователям не нравится начинать диалоговое взаимодействие заново, когда обнаруживается состояние гонки (race condition). Они могут потребовать пессимистической блокировки: чтобы во время редактирования данных товара пользователем A пользова-
552 Проектирование клиент-серверных приложений тель B не мог увидеть этого товара в диалоге редактирования. Главная проблема не в оптимистической проверке версий по окончании диалогового взаимодействия; проблема в том, что все изменения будут потеряны при запуске нового диалогового взаимодействия. Вместо простого вывода сообщения об ошибке при попытке одновременного изменения данных можно создать диалог, позволяющий пользователю сохранить ставшие недействительными изменения, вручную выполнить слияние с измене ниями, сделанными другим пользователем, а затем сохранить итоговый результат. Но предупреждаем, что реализация такой функциональности может потребовать большого количества времени и Hibernate не сильно вам в этом поможет.
Недостатком такого подхода является необходимость разработки толстого клиента, решения проблем сетевого взаимодействия и сериализации данных. Сложность реализации переносится со стороны сервера на клиента, и вы должны оптимизировать связь клиента с сервером. Если вместо EJB-клиента вы разрабатываете клиента на JavaScript, который должен работать в нескольких браузерах или использоваться как обычное приложение в различных (мобильных) операционных системах, сделать это может быть очень трудно. Мы советуем использовать такую архитектуру, когда толстый клиент работает в популярных браузерах, где пользователи будут загружать самую последнюю версию клиентского приложения каждый раз, когда они заходят на сайт. Развертывание обычных приложений на нескольких платформах, их сопровождение и обновление могут быть серьезным бременем даже в корпоративных сетях среднего размера, где есть возможность управлять пользовательским окружением. Работая вне среды EJB, вы должны будете реализовать сериализацию и передачу отсоединенных сущностей между клиентом и сервером. Можете ли вы настроить сериализацию и десериализацию экземпляра Item? Что произойдет, если клиент будет написан не на Java? Мы рассмотрим этот вопрос в разделе 19.4. Далее мы повторно реализуем тот же сценарий, но с применением совершенно иной стратегии. Теперь сервер будет хранить состояние диалога с приложением, а клиентом будет простое устройство ввода/вывода. Это – архитектура с тонким клиентом и сервером, хранящим состояние (stateful server).
18.3. Разработка сервера с сохранением состояния Следующее наше приложение останется таким же простым. Оно будет поддерживать те же варианты использования, что и прежде: редактирование товара и размещение ставки. Пользователи приложения не заметят разницы; консольный EJBклиент будет по-прежнему выглядеть, как на рис. 18.2 и 18.4. Сервер будет отвечать за преобразование данных для отображения в формат, понятный тонкому клиенту, например в страницу HTML, отображаемую браузе-
Разработка сервера с сохранением состояния 553 ром. Клиент будет передавать данные пользовательского ввода напрямую серверу, например при отправке формы HTML. Сервер должен расшифровать и преобразовать полученные данные в формат, более пригодный для использования в высокоуровневых операциях предметной модели. Однако мы упростим эту часть и используем удаленные вызовы методов в EJB-клиенте. Сервер также будет запоминать состояние диалогового взаимодействия, сохраняя его в объекте сеанса, связанном с конкретным клиентом. Обратите внимание, что сеанс существует дольше одного диалогового взаимодействия; пользователь может участвовать в нескольких диалоговых взаимодействиях в течение сеанса. Но если пользователь закроет клиентское приложение, не завершив диалогового взаимодействия, данные этого диалога должны быть в какой-то момент очищены. Для решения этой проблемы сервер обычно использует время ожидания; например, сервер может удалить пользовательский сеанс со всеми данными после определенного периода бездействия. Эта работа как раз подходит для сеансовых компонентов EJB с сохранением состояния – они идеально подходят для реализации данной архитектуры. Запомнив все эти фундаментальные особенности, реализуем первый вариант использования: редактирование товара.
18.3.1. Редактирование информации о товаре Новый клиент все так же выводит список товаров, а пользователь выбирает один из них. Это простейшая часть приложения, и серверу не нужно хранить никаких данных о состоянии диалога. Взгляните на последовательность вызовов на рис. 18.6.
Рис. 18.6 Клиент получает данные, готовые для отображения
Поскольку клиент очень простой, он не должен знать ничего о классе сущности Item. Он загружает список List объектов передачи данных ItemBidSummary:
554 Проектирование клиент-серверных приложений Файл: /apps/app-stateful-server/src/test/java/org/jpwh/test/stateful/ AuctionServiceTest.java List itemBidSummaries = auctionService.getSummaries();
Сервер реализует эту функциональность с помощью компонента без состояния, поскольку пока нет необходимости сохранять состояние диалога: Файл: /apps/app-stateful-server/src/main/java/org/jpwh/stateful/ AuctionServiceImpl.java @javax.ejb.Stateless @javax.ejb.Local(AuctionService.class) @javax.ejb.Remote(RemoteAuctionService.class) public class AuctionServiceImpl implements AuctionService { @Inject protected ItemDAO itemDAO;
}
@Override public List getSummaries() { return itemDAO.findItemBidSummaries(); }
Даже в серверной архитектуре с сохранением состояния всегда будет происходить множество коротких диалоговых взаимодействий с приложением, не требующим сохранения состояния на сервере. Это нормально, и важно понимать, что хранение состояния на сервере стоит ресурсов. Если реализовать операцию getSummaries(), используя сеансовый компонент с сохранением состояния, вы лишь впустую потратите ресурсы. Компонент с сохранением состояния понадобится только для единственной операции, после чего он будет занимать память, пока контейнер не избавится от него. Архитектура сервера с состоянием не обязывает применять одни лишь компоненты с состоянием. Далее клиентское приложение выводит список объектов ItemBidSummary, содержащий только идентификатор, описание и максимальную ставку каждого товара. Это именно то, что пользователь видит на экране, как показано на рис. 18.2. После чего пользователь введет идентификатор товара и начнет диалоговое взаимодействие. Схема этого диалогового взаимодействия приводится на рис. 18.7. Клиент сообщает серверу, что тот должен начать диалоговое взаимодействие, посылая ему значение идентификатора : Файл: /apps/app-stateful-server/src/test/java/org/jpwh/test/stateful/ AuctionServiceTest.java itemService.startConversation(itemId);
Здесь уже не вызывается служба AuctionService без состояния из предыдущего раздела. Новая служба ItemService – это компонент с сохранением состояния; сервер будет создавать его экземпляры и назначать их отдельно для каждого клиента. Эта служба реализуется с использованием сеансового компонента с состоянием:
Разработка сервера с сохранением состояния 555
Рис. 18.7 Клиентское приложение задает границы диалогового взаимодействия на сервере Файл: /apps/app-stateful-server/src/main/java/org/jpwh/stateful/ ItemServiceImpl.java @javax.ejb.Stateful(passivationCapable = false) Минуты @javax.ejb.StatefulTimeout(10) @javax.ejb.Local(ItemService.class) @javax.ejb.Remote(RemoteItemService.class) public class ItemServiceImpl implements ItemService { @PersistenceContext(type = EXTENDED, synchronization = UNSYNCHRONIZED) protected EntityManager em; @Inject protected ItemDAO itemDAO; @Inject protected BidDAO bidDAO; // Состояние диалогового взаимодействия на сервере protected Item item; // ... @Override public void startConversation(Long itemId) { item = itemDAO.findById(itemId); if (item == null) throw new EntityNotFoundException( "No Item found with identifier: " + itemId ); } }
// ...
556 Проектирование клиент-серверных приложений Этот класс имеет множество аннотаций, определяющих взаимодействие контейнера с компонентом. Поскольку для компонента задано время ожидания 10 минут, сервер удалит и уничтожит его, если он не будет вызываться в течение этого времени. Это позволяет избавиться от диалоговых взаимодействий с длительным периодом бездействия, когда, например, пользователь долго не использует клиентского приложения. Также для компонента EJB отключено пассивирование (passivation): контейнер EJB может сериализовать и сохранить компонент с состоянием на диск для экономии памяти или передачи его другому узлу кластера для восстановления сеанса в случае ошибки. Пассивирование не затронет лишь одного поля – EntityManager. Контекст хранения присоединяется к этому компоненту благодаря параметру EXTENDED; также класс EntityManager не реализует интерфейс java.io.Serializable. ЧАСТО ЗАДАВАЕМЫЕ ВОПРОСЫ: Почему нельзя сериализовать EntityManager? Нет никаких технических ограничений, почему контекст хранения и объект EntityManager не могут быть сериализованы. Конечно, после десериализации объект EntityManager должен быть присоединен к правильному экземпляру EntityManagerFactory на целевой машине, но это уже особенности реализации. Пассивирование контекста хранения до сих пор было вне поля зрения спецификаций JPA и Java EE. Тем не менее большинство реализаций позволяет сериализовать и корректно десериализовать контекст хранения. EntityManager в Hibernate может быть сериализован и десериализован и правильно присоединен к нужной единице хранения после десериализации. Используя Hibernate с сервером Wildfly, вы могли бы использовать пассивирование в предыдущем примере, получая возможность восстановления сеанса в случае ошибки, а также бесперебойную работу сервера с состоянием и расширенные контексты хранения. Тем не менее эта функциональность не стандартизована; как мы увидим далее, такая стратегия препятствует масштабированию. Были времена, кода даже в Hibernate не было возможности сериализовать EntityManager. Вы можете столкнуться с устаревшими фреймворками, пытавшимися обойти это ограничение, такими как Seam, который использует ManagedEntityInterceptor. Вам следует избегать этого и находить более простые решения, такие как перенаправление запросов на один узел кластера (sticky session), архитектуру сервера с состоянием или внедрение зависимостей (CDI) при диалоговом взаимодействии на стороне сервера, которое мы обсудим в следующей главе на примере контекста хранения, связанного с запросом.
Аннотация @PersistenceContext объявляет, что этот компонент с состоянием нуждается в экземпляре EntityManager и контейнер должен расширять время жизни контекста хранения до границ жизненного цикла компонента. Режим расширения доступен только для EJB-компонентов с состоянием. Без этого контейнер будет создавать и закрывать контекст хранения при подтверждении каждой транзакции. Но здесь требуется, чтобы контекст хранения оставался
Разработка сервера с сохранением состояния 557 открытым за границами транзакции и был привязан к экземпляру сеансового компонента с состоянием. Более того, нужно предотвратить автоматическое выталкивание контекста во время подтверждения транзакции, поэтому используется параметр UNSYNCHRONIZED. Hibernate вытолкнет контекст хранения после его подключения к транзакции вручную. Теперь Hibernate не будет автоматически записывать изменения в хранимых экземплярах сущностей в базу данных; вместо этого он будет накап ливать изменения, пока вы не решите записать все разом. В начале диалогового взаимодействия сервер загрузит экземпляр Item, представляющий состояние диалога, и сохранит его в поле класса (рис. 18.7). Объекту ItemDAO также необходим экземпляр EntityManager; вспомните, что он отмечен аннотацией @PersistenceContext без дополнительных параметров. Правила передачи контекста хранения в EJB такие же, как и раньше: Hibernate передает контекст хранения вместе с контекстом транзакции. Контекст хранения будет передан в ItemDAO вместе с транзакцией, запущенной вызовом метода startConversation(). После выхода из метода startConversation() транзакция подтверждается, но контекст хранения не выталкивается и не закрывается. Экземпляр ItemServiceImpl ожидает следующего обращения клиента. Следующий вызов от клиента просит сервер изменить название товара : Файл: /apps/app-stateful-server/src/test/java/org/jpwh/test/stateful/ AuctionServiceTest.java itemService.setItemName("Pretty Baseball Glove");
На стороне сервера транзакция запустится вызовом метода setItemName(). Но поскольку никакие ресурсы транзакции не используются (ни вызовы DAO, ни вызовы EntityManager), изменится только объект Item, представляющий диалоговое состояние: Файл: /apps/app-stateful-server/src/main/java/org/jpwh/stateful/ ItemServiceImpl.java public class ItemServiceImpl implements ItemService { // ... @Override public void setItemName(String newName) { item.setName(newName); } После подтверждения транзакции контекст хранения не выталкивается, }
// ...
поскольку он рассинхронизирован
Обратите внимание, что экземпляр Item по-прежнему находится в хранимом состоянии, потому что контекст хранения еще открыт! Но из-за рассинхронизации он не обнаружит изменений в экземпляре Item, поскольку не будет выталкиваться при подтверждении транзакции.
558 Проектирование клиент-серверных приложений Наконец, клиентское приложение завершает диалоговое взаимодействие, предлагая серверу сохранить изменения (рис. 18.7): Файл: /apps/app-stateful-server/src/test/java/org/jpwh/test/stateful/ AuctionServiceTest.java itemService.commitConversation();
Теперь на сервере можно записать изменения в базу данных и очистить диалоговое состояние: Файл: /apps/app-stateful-server/src/main/java/org/jpwh/stateful/ ItemServiceImpl.java public class ItemServiceImpl implements ItemService {
}
@Override @javax.ejb.Remove Компонент удаляется после завершения этого метода public void commitConversation() { em.joinTransaction(); Контекст хранения соединяется с текущей транзакцией }
и выталкивается после выхода из метода, сохраняя изменения
На этом реализация первого варианта использования закончена. Мы опускаем реализацию второго варианта (размещение ставки) и отсылаем вас к коду примера за подробностями. Код второго варианта очень похож на код первого, поэтому у вас не должно возникнуть проблем с его пониманием. Важно, чтобы вы понимали, как работают контекст хранения и как действуют транзакции в EJB. ПРИМЕЧАНИЕ В EJB существуют дополнительные правила передачи контекста хранения между различными типами компонентов. Они довольно сложны, и мы никогда не видели хороших вариантов для их применения. К примеру, вряд ли вы станете вызывать компонент EJB с состоянием из компонента EJB без состояния. Еще одну трудность представляют методы EJB с отключенными или необязательными транзакциями, которые также влияют на передачу контекста хранения через вызовы компонентов. Мы рассказывали об этих правилах в предыдущем издании этой книги. Советуем придерживаться только тех стратегий, что были показаны в этой главе, ничего не усложняя.
Давайте обсудим некоторые различия между архитектурами с состоянием и без состояния.
18.3.2. Анализ приложений с сохранением состояния Так же, как при анализе приложения без состояния, рассмотрим сначала реализацию единицы работы с точки зрения пользователя. В частности, нужно понять, как в диалоговом взаимодействии реализована атомарность и как представить последовательность действий в виде одной единицы работы. В какой-то момент – обычно во время последнего запроса в рамках диалогового взаимодействия – происходят подтверждение и запись изменений в базу данных.
Разработка сервера с сохранением состояния 559 Диалоговое взаимодействие будет атомарным, если не присоединять расширенного экземпляра EntityManager с транзакцией до последнего диалогового события. Если читать данные в рассинхронизированном режиме, состояние объектов проверяться не будет, так же как не будет выталкиваться контекст хранения. Пока контекст открыт, можно выполнять отложенную загрузку данных, обращаясь к прокси-объектам и незагруженным коллекциям, что, очевидно, довольно удобно. Загруженный объект Item, как и прочие данные, устаревает, если пользователю требуется длительное время для выполнения следующего запроса. Во время диалогового взаимодействия вам, возможно, придется выполнять операцию refresh() для некоторых управляемых экземпляров сущностей, чтобы получать обновления из базы данных, как объяснялось в разделе 10.2.6. С другой стороны, вы можете выполнять обновления для отката изменений, сделанных в ходе диалогового взаимодействия. Например, если пользователь поменяет поле Item#name, а затем решит отменить изменение, вы можете обновить хранимый экземпляр Item вызовом метода refresh(), который извлечет из базы данных старое название товара. Эта приятная особенность расширенного контекста хранения позволяет экземпляру Item всегда находиться в управляемом состоянии. Точки сохранения в диалоговых взаимодействиях Вам могут быть знакомы точки сохранения (savepoints) в транзакциях JDBC: после изменения некоторых данных в рамках транзакции создается точка сохранения; позже вы можете откатить транзакцию до этой точки, отказываясь лишь от части сделанных изменений, но сохраняя все, что было сделано до создания точки восстановления. К сожалению, Hibernate не поддерживает ничего, похожего на точки восстановления, для контекста хранения. Экземпляр сущности можно откатить только до состояния в базе данных, используя метод refresh(). В Hibernate можно использовать обычные точки сохранения в транзакциях JDBC (для этого потребуется экземпляр Connection; см. раздел 17.1), но они не помогут сделать откат в диалоговом взаимодействии.
Серверная архитектура с сохранением состояния труднее поддается горизонтальному масштабированию. Если на сервере произойдет сбой, текущее состояние диалогового взаимодействия, как и сеанс, целиком будет потеряно. Репликация сеанса на несколько серверов – это дорогостоящая операция, поскольку каждое изменение в сеансе на одном сервере вызывает сетевое взаимодействие с другими (потенциально всеми) серверами. Сериализация расширенного контекста хранения невозможна при работе с компонентами EJB и расширенными экземплярами EntityManager. При использовании компонентов EJB с состоянием и расширенного контекста хранения в кластере можно рассмотреть вариант применения прикрепленного сеанса (sticky session), когда запросы конкретного клиента всегда направляются на один физический сервер. Это позволит справиться с растущей нагрузкой путем добавления серверов, но пользователь должен быть готов к потере данных в случае сбоя на сервере.
560 Проектирование клиент-серверных приложений С другой стороны, сервер с состоянием может выступать в качестве первой линии кэширования со своими расширенными контекстами хранения в сеансах пользователей. Как только экземпляр Item будет загружен во время диалогового взаимодействия с конкретным пользователем, он не будет загружаться снова из базы данных в рамках этого взаимодействия. Это может стать отличным инструментом для снижения нагрузки на сервер базы данных (самый дорогой слой с точки зрения масштабирования). Стратегия применения расширенного контекста хранения требует от сервера больше памяти, чем хранение только отсоединенных экземпляров, потому что контекст хранения в Hibernate содержит копии всех управляемых экземпляров. Вам может потребоваться вручную отсоединять управляемые экземпляры методом detach() для управления тем, что хранится в контексте, или отключать проверку состояния объектов и хранение копий (сохраняя при этом возможность отложенной загрузки), как объяснялось в разделе 10.2.8. Конечно, существуют альтернативные реализации тонких клиентов и серверов с состоянием. Можно использовать контекст хранения, связанный с запросом, управляя отсоединенными (нехранимыми) экземплярами сущностей на сервере вручную. Очевидно, это можно сделать путем отсоединения и слияния, но потребует больших затрат на реализацию. Одно из главных преимуществ расширенного контекста хранения – прозрачная отложенная загрузка (даже между запросами) – больше не будет доступна. В следующей главе мы покажем реализацию такого сервера с состоянием, контекст хранения которого в CDI и JSF привязан к запросу, и вы сможете сравнить это с функциональностью расширенного контекста хранения EJB, которую мы показали в этой главе. Системы с тонким клиентом, как правило, создают большую нагрузку на сервер, чем толстые клиенты. Каждый раз, когда пользователь взаимодействует с приложением, любое клиентское событие будет отправлять запрос по сети. Это может происходить при каждом нажатии на кнопку мыши в веб-приложении. Только сервер знает о состоянии диалогового взаимодействия, и он должен подготовить и отобразить информацию, которую увидит пользователь. Толстый клиент, напротив, может загрузить необработанные данные в одном запросе, преобразовать их и уже на месте привязать их к пользовательскому интерфейсу. Диалог в толстом клиенте может накапливать изменения на стороне клиента и отправлять запрос по сети только в конце взаимодействия, когда нужно сохранить изменения в базу данных. Другая проблема с тонким клиентом заключается в параллельных диалоговых взаимодействиях одного пользователя: что произойдет, если пользователь изменит два элемента одновременно, например в двух вкладках браузера? Это будет означать, что пользователь запустит два диалоговых взаимодействия на сервере. Сервер должен будет разделить данные в сеансе пользователя, в зависимости от диалогового взаимодействия. Следовательно, клиентские запросы должны будут содержать некоторое подобие идентификатора диалогового взаимодействия, чтобы можно было извлекать корректное состояние диалога из сеанса пользовате-
Резюме 561 ля при каждом запросе. При работе с клиентами и серверами, основанными на EJB, это происходит автоматически, но вряд ли эта функциональность встроена в ваш любимый фреймворк веб-приложений (если только это не JSF и CDI, как вы узнаете в следующей главе). Одним из самых больших преимуществ сервера с состоянием является слабая зависимость от клиентской платформы; если клиентом является простой терминал ввода/вывода, будет меньше шансов, что что-то пойдет не так. Единственное место проверки данных и безопасности будет на сервере. Не будет никаких проб лем с развертыванием; вы сможете обновлять приложение на сервере, не затрагивая клиентов. Сегодня у тонкого клиента немного преимуществ, и количество установок серверов с состоянием снижается. Это особенно заметно на рынке веб-приложений, где легкость масштабирования является решающим фактором.
18.4. Резюме В этой главе мы реализовали простые диалоговые взаимодействия – единицы работы с точки зрения пользователей приложения. Рассмотрели проекты клиента и сервера с состоянием и без состояния и узнали, как Hibernate вписывается в каждую из них. Вы можете работать либо с отсоединенными экземплярами сущностей, либо с расширенным контекстом хранения, распространяющимся на все диалоговое взаимодействие.
Глава
19 Создание веб-приложений
В этой главе: интеграция JPA с CDI и JSF; просмотр таблиц баз данных; реализация длительных диалоговых взаимодействий; настройка сериализации сущностей.
В этой главе вы узнаете, как применять Hibernate в типичном веб-приложении. Существуют десятки фреймворков для разработки веб-приложений на Java, поэтому мы заранее приносим извинения, если пройдем мимо вашего любимого фреймворка. Мы рассмотрим применение JPA в стандартном окружении Java Enterprise Edition, в частности в сочетании со следующими стандартами: внедрение контекста и зависимостей (CDI), Java Server Faces (JSF) и Java API для веб-служб на основе REST (JAX-RS). Мы, как всегда, продемонстрируем шаблоны, которые можно применять также в нестандартных окружениях. Сначала мы еще раз обратимся к уровню хранения и продемонстрируем применение CDI в классах DAO. После чего расширим эти классы, реализовав обобщенное решение для сортировки и постраничного вывода данных. Это решение можно использовать, когда потребуется отобразить табличные данные независимо от выбранного фреймворка. Далее, опираясь на уровень хранения, мы напишем полностью работающее JSF-приложение и познакомимся с областью видимости диалога в Java EE (conversation scope), в которой CDI, взамодействуя с JSF, поддерживает простую модель серверных компонентов с состоянием. Если вам не приглянулись показанные в прошлой главе EJB-компоненты с состоянием и расширенным контекстом хранения, диалоговые взаимодействия с отсоединенным состоянием сущности на сервере, вероятно, вам понравятся больше. И наконец, если вам нравится создавать веб-приложения с толстыми клиентами, серверами без состояния и такими фреймворками, как JAX-RS, GWT или AngularJS, мы научим вас сериализовать экземпляры сущностей JPA в форматы XML и JSON. А начнем мы с переноса реализации уровня хранения с EJB на CDI.
Интеграция JPA и CDI 563
19.1. Интеграция JPA и CDI Стандарт CDI определяет механизм типизированного внедрения зависимостей, а также систему управления жизненным циклом компонентов в среде выполнения Java EE. В предыдущей главе вы уже использовали аннотацию @Inject для связывания компонентов ItemDAO и BidDAO с классами служб EJB. JPA-аннотация @PersistenceContext, которую мы помещали внутрь классов DAO, представляет еще один, особый случай внедрения зависимостей: вы просите контейнер среды выполнения внедрить экземпляр EntityManager и автоматически управлять им. Этот экземпляр, EntityManager, будет управляться контейнером. Но есть еще подводные камни, такие как передача контекста хранения и правила распространения транзакций, которые мы обсудили в предыдущей главе. Такие правила удобны, когда все классы служб и DAO являются компонентами EJB; но если вы не используете EJB, вам, возможно, не захочется следовать этим правилам. С помощью управляемого приложением экземпляра EntityManager можно определить собственные правила управления контекстом хранения, его передачи и внедрения. Сейчас мы перепишем классы DAO, как обычные управляемые компоненты CDI, которые очень похожи на EJB: простые Java-классы с дополнительными аннотациями. Нужно лишь с помощью аннотации @Inject внедрить экземпляр EntityManager, избавившись от @PersistenceContext, и получить тем самым полный контроль над контекстом хранения. Но прежде, чем внедрить собственный экземпляр EntityManager, его нужно создать.
19.1.1. Создание экземпляра EntityManager Продюсер (producer) в терминологии CDI – это фабрика, управляющая созданием экземпляра, которую контейнер среды выполнения будет вызывать, когда приложению понадобится экземпляр в заданной области видимости. К примеру, контейнер создаст экземпляр в области видимости приложения только один раз за весь жизненный цикл приложения. Экземпляр в области видимости запроса (request-scoped) будет создаваться контейнером при получении запроса от клиента, а экземпляр в области видимости сеанса (session-scoped) – отдельно для каждого сеанса пользователя. Спецификация CDI задает отображение между абстрактными понятиями запроса и сеанса и реальными объектами запроса и сеанса в сервлете. Не забывайте, что JSF и JAX-RS построены на основе сервлетов, поэтому CDI отлично подходит для этих фреймворков. Другими словами, не беспокойтесь об этом: в окружении Java EE вся работа по интеграции уже сделана за вас. Давайте напишем продюсера для экземпляров EntityManager в области видимости запроса: Файл: /apps/app-web/src/main/java/org/jpwh/web/dao/ EntityManagerProducer.java @javax.enterprise.context.ApplicationScoped public class EntityManagerProducer {
Нужен только 1 продюсер
564 Создание веб-приложений @PersistenceUnit Получение единицы хранения private EntityManagerFactory entityManagerFactory; Получение экземпляра EntityManager @javax.enterprise.inject.Produces @javax.enterprise.context.RequestScoped public EntityManager create() { return entityManagerFactory.createEntityManager(); }
}
public void dispose( @javax.enterprise.inject.Disposes EntityManager entityManager) { if (entityManager.isOpen()) entityManager.close(); }
Закрытие контекста хранения
Эта аннотация CDI объявляет, что во всем приложении должен иметься только один продюсер: будет существовать единственный экземпляр EntityManagerProducer. Среда выполнения Java EE предоставит единицу хранения, определенную в файле persistence.xml, которая также является компонентом области видимости приложения. (Если CDI используется вне окружения Java EE, можно вызвать статический фабричный метод Persistence.createEntityManagerFactory()). Как только приложению потребуется экземпляр EntityManager, будет вызван метод create(). Контейнер повторно использует один экземпляр EntityManager в течение всего запроса, обрабатываемого сервером. (Если забыть поместить аннотацию @RequestScoped перед методом, экземпляр EntityManager будет иметь область видимости приложения, как и класс продюсера!) После завершения обработки запроса, при удалении его контекста, контейнер CDI вызовет этот метод, чтобы избавиться от экземпляра EntityManager. Поскольку вы сами создали этот управляемый приложением контекст хранения (см. раздел 10.1.2), вы и отвечаете за его закрытие.
Частой ошибкой при работе с классами, использующими аннотации CDI, является неправильный импорт аннотаций. В Java EE 7 существуют две аннотации с именем @Produces; вторая находится в пакете javax.ws.rs (спецификация JAX-RS). Ее семантика отличается от аннотации продюсера CDI, и вы можете часами искать ошибку, если импортировали не ту аннотацию. Другой такой же аннотацией является @RequestScoped из пакета javax.faces.bean (специфиакация JSF). Как и большинство устаревших аннотаций JSF для управления компонентами из пакета javax.faces.bean, ее не стоит использовать, если доступна более современная альтернатива из CDI. Мы надеемся, что будущие спецификации Java EE устранят эту двусмысленность. Теперь у нас есть фабрика для создания экземпляров EntityManager, управляемых приложением, и контекста хранения с областью видимости запроса. Теперь нужно придумать, как сообщать экземпляру EntityManager о системных транзакциях.
Интеграция JPA и CDI 565
19.1.2. Присоединение экземпляра EntityManager к транзакциям Когда серверу потребуется обработать запрос сервлета, контейнер автоматически создаст экземпляр EntityManager при первой необходимости его внедрения. Помните, что созданный вручную экземпляр EntityManager автоматически присоединится к системной транзакции, только если та уже началась. В противном случае он будет рассинхронизирован: вы будете читать данные в режиме автоматического подтверждения (auto-commit), и Hibernate не будет выталкивать контекста хранения. Не всегда очевидно, в какой момент контейнер вызовет продюсера EntityMa nager или когда точно во время обработки запроса произойдет внедрение EntityManager. При обработке запроса вне системной транзакции получаемый объект EntityManager всегда будет рассинхронизирован. Следовательно, мы должны сделать так, чтобы экземпляр EntityManager узнал о системной транзакции. Для этой цели в суперинтерфейсе уровня хранения имеется следующий метод: Файл: /apps/app-web/src/main/java/org/jpwh/web/dao/GenericDAO.java public interface GenericDAO extends Serializable { void joinTransaction(); }
// ...
Мы должны вызвать этот метод в каждом классе DAO перед сохранением данных, когда точно известно, что он будет вызван в рамках транзакции. Напомним, что при попытке записи данных Hibernate возбудит исключение TransactionRequiredException, напоминая, что экземпляр EntityManager был создан перед началом транзакции и не знает о ней. Если вы хотите потренировать свои навыки в CDI, можете попробовать реализовать эту функциональность с помощью декораторов или перехватчиков CDI. Давайте реализуем новый метод интерфейса GenericDAO, связав экземпляр EntityManager с классами DAO.
19.1.3. Внедрение экземпляра EntityManager Старая реализация GenericDAOImpl полагалась на аннотацию @PersistenceContext для внедрения объекта EntityManager в поле класса или на вызов setEntityMana ger() перед использованием класса DAO. С помощью CDI можно использовать более безопасную технику внедрения в конструктор: Файл: /apps/app-web/src/main/java/org/jpwh/web/dao/GenericDAOImpl.java public abstract class GenericDAOImpl implements GenericDAO { protected final EntityManager em; protected final Class entityClass;
566 Создание веб-приложений protected GenericDAOImpl(EntityManager em, Class entityClass) { this.em = em; this.entityClass = entityClass; } public EntityManager getEntityManager() { return em; }
}
@Override public void joinTransaction() { if (!em.isJoinedToTransaction()) em.joinTransaction(); } // ...
Каждый, кто захочет создать экземпляр класса DAO, должен будет передать ему объект EntityManager. Такое определение инварианта класса дает более весомые гарантии; следовательно, несмотря на то что в наших примерах мы часто используем внедрение в поля классов, вы должны в первую очередь рассматривать возможность внедрения в конструкторы. (Мы не делаем этого в некоторых примерах, поскольку от этого они стали бы только длиннее, а книга уже и так не маленькая). В конкретных подклассах (DAO сущностей) следует объявить требуемое внед рение в конструктор: Файл: /apps/app-web/src/main/java/org/jpwh/web/dao/ItemDAOImpl.java public class ItemDAOImpl extends GenericDAOImpl implements ItemDAO { @Inject public ItemDAOImpl(EntityManager em) { super(em, Item.class); } // ... }
Когда приложению потребуется объект ItemDAO, среда выполнения CDI обратится к продюсеру EntityManagerProducer, а затем вызовет конструктор ItemDAO Impl. В рамках одного запроса контейнер повторно использует один и тот же объект EntityManager для внедрения в каждый экземпляр DAO. ЧАСТО ЗАДАВАЕМЫЕ ВОПРОСЫ: Как в CDI работать с несколькими единицами хранения? При работе с несколькими базами данных – разными единицами хранения – можно использовать квалификаторы CDI, чтобы различать их. Квалификатор – это произ-
Сортировка и постраничная выборка данных 567 вольная аннотация. Вы создаете аннотацию вроде @BillingDatabase и отмечаете ее как квалификатор. Затем помещаете ее рядом с аннотацией @Produces перед методом, создающим экземпляр EntityManager для этой конкретной единицы хранения. Теперь, когда понадобится этот экземпляр EntityManager, вы должны будете добавить аннотацию @BillingDatabase рядом с @Inject.
В какой области видимости находится ItemDAO? Поскольку область видимости классов реализации не задана, это зависит от конкретной ситуации. Экземпляр ItemDAO создается, когда он кому-либо нужен, поэтому экземпляр ItemDAO будет находиться в той же области видимости, что и вызывающий код, и будет принадлежать вызывающему объекту. Это хорошее решение для реализации интерфейса уровня хранения, поскольку перекладывает решение о выборе области видимости на верхний уровень, состоящий из служб, обращающихся к уровню хранения. Теперь с помощью аннотации @Inject можно внедрить экземпляр ItemDAO в поле класса службы. Но, прежде чем воспользоваться уровнем хранения на основе CDI, давайте реализуем еще поддержку сортировки и постраничной выборки данных.
19.2. Сортировка и постраничная выборка данных Очень распростаненным требованием являются загрузка данных из базы и их отображение на веб-странице в табличном виде. Но часто также требуется реализовать динамическую постраничную выборку и сортировку данных: поскольку запрос возвращает больше данных, чем можно отобразить на одной странице, вы должны показывать только часть из них. Вы отображаете определенное количество записей, давая пользователю возможность перехода к следующему, предыдущему, первому или последнему набору записей. Пользователь также ожидает сохранения порядка сортировки при переключении между страницами; пользователь должен иметь возможность, щелкнув мышкой на заголовке колонки, отсортировать строки в таблице по значениям в этой колонке. Обычно сортировать можно либо по возрастанию, либо по убыванию; направление можно менять последовательными щелчками на заголовке колонки. Сейчас мы реализуем обобщенное решение для постраничной выборки, основанное на метамодели хранимых классов, предоставляемой JPA. Постраничную выборку можно реализовать двумя способами: с помощью приема смещения (offset) или поиска (seek). Давайте рассмотрим их отличия и определимся с реализацией.
19.2.1. Реализация постраничной выборки с помощью смещения или поиска На рис. 19.1 показан пример пользовательского интерфейса с постраничным отображением на основе смещений. У нас имеется довольно много аукционных товаров, но на одной странице можно отобразить только три записи. Сейчас мы
568 Создание веб-приложений находимся на первой странице; приложение также динамически отображает ссылки на другие страницы. Результаты отсортированы по возрастанию наименования товара. Вы можете щелкнуть на заголовке колонки, чтобы отсортировать по убыванию (или возрастанию) наименования, даты окончания аукциона или наибольшему значению ставки. Щелчок на наименовании товара в таблице откроет диалог просмотра данных о товаре, в котором вы сможете сделать ставку. Как раз этот вариант использования мы и реализуем в этой главе.
Рис. 19.1 Отображение страниц каталога с использованием приема смещений
Для создания этой страницы применялись запросы к базе данных с заданными смещением и количеством извлекаемых записей. Для этого вызывались методы Java Persistence API: Query#setFirstResult() и Query#setMaxResults(), обсуждавшиеся в разделе 14.2.4. Сначала пишется запрос, а затем фреймворку Hibernate предоставляется возможность добавить в него ограничения на смещение и количество извлекаемых записей, в зависимости от используемого диалекта SQL. Теперь пришло время рассмотреть альтернативный подход с применением поиска, как показано на рис. 19.2. Здесь у пользователя нет возможности перейти на произвольную страницу с заданным смещением; он может листать только вперед, переходя на следующую страницу. Это может выглядеть как ограничение, но вы, вероятно, видели или даже использовали такой способ постраничной выборки, когда требовалась бесконечная прокрутка. Можно, например, автоматически подгрузить и отобразить следующую страницу с данными, когда пользователь дойдет до конца таблицы/экрана. Метод поиска основан на особом ограничении в запросе, извлекающем данные. Когда придет время загрузить следующую страницу, выполнится поиск всех товаров с названиями «больше, чем [Coffee Machine]». Движение вперед будет осуществляться не за счет установки смещения в результатах методом setFirstResult(), а за счет ограничения результатов на основе отсортированных значений какого-либо ключа. Если вы незнакомы с постраничной выборкой на основе поиска, иногда называемой ключевой (keyset paging), мы уверены, что она не покажется вам сложной после тех запросов, которые вы увидите далее в этой главе.
Сортировка и постраничная выборка данных 569
Рис. 19.2 Отображение страниц каталога с использованием приема поиска следующей страницы
Давайте обсудим недостатки и преимущества обоих подходов. Конечно, можно реализовать бесконечную прокрутку, используя постраничную выборку на основе смещения, или переход к конкретной странице, используя метод поиска; но у каждой из них есть свои сильные и слабые стороны: метод смещения удобен, если пользователю нужно перейти непосредственно к конкретной странице. К примеру, большинство поисковых движков поддерживает переход прямо к странице 42 в результатах запроса или сразу к последней странице. Поскольку мы с легкостью можем рассчитать смещение и количество извлекаемых записей в зависимости от требуемого номера страницы, мы без труда сможем это реализовать. Реализовать подобный пользовательский интерфейс с помощью метода поиска гораздо сложнее; необходимо заранее знать искомое значение. Поскольку неизвестно, какой товар предшествует странице 42, мы не сможем выбрать все товары с на именованием «больше, чем X». Метод поиска подходит только для пользовательских интерфейсов, где пользователи переходят в прямом или обратном направлении от страницы к странице в списке или таблице с данными и где известно последнее (или первое) значение, показанное пользователю; отличный вариант использования для постраничной выборки на основе поиска основан на ключевых значениях, которые не нужно запоминать. Например, все пользователи с именами, начинающимися на C, могут отобра жаться на одной странице, а пользователи с именами, начинающимися на D, – на следующей. Также каждая страница может отображать только товары, преодолевшие пороговое значение максимальной ставки; метод смещений работает гораздо хуже для страниц, находящихся в конце. При переходе к странице 5000 база данных должна подсчитать все строки и подготовить 5000 страниц данных, прежде чем сможет пропустить предыдущие 4999. Типичное решение этой проблемы заключается в ограничении количества страниц, на которые пользователь может непосредственно перейти: например, разрешить пользователю переход только на первые 100 страниц, заставляя его уточнить ограничения запроса для получения меньшего количества результатов. Метод поиска обычно работает быстрее
570 Создание веб-приложений метода смещений даже для самых первых страниц. Оптимизатор запросов базы данных может сразу перейти к началу требуемой страницы и эффективно ограничить сканируемую область индекса. Записи, показанные на предыдущих страницах, никак не учитываются и не подсчитываются; иногда метод смещений может показывать некорректные результаты. Хотя результат будет соответствовать состоянию базы данных, пользователям он может казаться некорректным. Когда приложение вставляет или удаляет записи во время просмотра данных пользователем, могут возникать аномалии. Представьте пользователя, который смотрит на страницу 1, в то время как другой добавляет данные, которые должны появиться на странице 1. Если теперь пользователь перейдет к странице 2, некоторые записи, которые он мог видеть на странице 1, перейдут на страницу 2. Если запись со страницы 1 была удалена, пользователь может не увидеть некоторых записей со страницы 2, поскольку они перейдут на страницу 1. В методе поиска таких аномалий не возникает; записи мистически не появляются и не исчезают. Сейчас мы покажем, как реализовать оба метода постраничной выборки путем расширения уровня хранения. Начнем с простой модели, хранящей номер текущей страницы и настройки сортировки табличных данных.
19.2.2. Реализация постраничной выборки в уровне хранения Для координации запросов и отображения страниц с данными нужно хранить информацию о размере страницы и знать, какая страница сейчас отображается. Ниже показан простейший класс для хранения этой информации; это абстрактный суперкласс, который подойдет как для метода смещений, так и для метода поиска: Файл: /apps/app-web/src/main/java/org/jpwh/web/dao/Page.java public abstract class Page { public static enum SortDirection { ASC, DESC } protected int size = -1;
Вывод всех записей
protected long totalRecords;
Подсчет количества записей
protected SingularAttribute sortAttribute; protected SortDirection sortDirection;
Сортировка записей
protected SingularAttribute[] allowedAttributes;
Список допустимых для сортировки атрибутов
// ...
}
abstract public TypedQuery createQuery( EntityManager em, CriteriaQuery criteriaQuery, Path attributePath );
Сортировка и постраничная выборка данных 571 Модель хранит размер каждой страницы и количество записей на странице. Значение –1 означает, что будут возвращены все записи без ограничений. Хранение количества всех записей необходимо для некоторых вычислений, например чтобы понять, существует ли «следующая» страница. Постраничная выборка всегда требует строго определенного порядка следования записей. Как правило, сортировка осуществляется по конкретному атрибуту класса сущности в порядке возрастания или убывания. Поле javax.persistence.metamodel.SingularAttribute в JPA ссылается на атрибут сущности или встраиваемого класса; оно не может ссылаться на коллекцию (результаты запроса нельзя «упорядочить по коллекции»). Список allowedAttributes задается во время создания модели страницы. Он определяет допустимые для сортировки атрибуты, которые можно использовать в запросах.
Мы опустили некоторые тривиальные методы класса Page – в основном это методы чтения/записи. Однако подклассы должны реализовать абстрактный метод createQuery(): он описывает применение настроек страницы к запросу CriteriaQuery перед выполнением. Сначала нужно связать интерфейс Page с уровнем хранения. Интерфейс DAO будет принимать экземпляр Page, когда потребуется осуществить постраничную выборку данных: Файл: /apps/app-web/src/main/java/org/jpwh/web/dao/ItemDAO.java public interface ItemDAO extends GenericDAO { List getItemBidSummaries(Page page); // ... }
Таблица данных для отображения будет показывать список List оъектов передачи данных ItemBidSummary. Результат запроса не так важен в этом примере; мы могли также извлечь и список экземпляров Item. Ниже показана часть реализации DAO: Файл: /apps/app-web/src/main/java/org/jpwh/web/dao/ItemDAOImpl.java public class ItemDAOImpl extends GenericDAOImpl implements ItemDAO { // ... @Override public List getItemBidSummaries(Page page) { Запрос на основе критериев CriteriaBuilder cb = getEntityManager().getCriteriaBuilder(); CriteriaQuery criteria = cb.createQuery(ItemBidSummary.class); Root i = criteria.from(Item.class); // Некоторые параметры запроса...
572 Создание веб-приложений // ...
} }
TypedQuery query = page.createQuery(em, criteria, i); return query.getResultList();
Окончательная подготовка запроса
// ...
Это самый обычный запрос на основе критериев, который вы видели много раз до этого. Окончательная подготовка запроса ложится на плечи полученного объекта Page.
Конкретная реализация интерфейса Page подготавливает запрос, устанавливая необходимые смещение, количество извлекаемых записей и параметры поиска.
Реализация метода смещений Ниже показана реализация стратегии постраничной выборки на основе сме щений: Файл: /apps/app-web/src/main/java/org/jpwh/web/dao/OffsetPage.java public class OffsetPage extends Page { protected int current = 1;
Текущая страница
// ... @Override public TypedQuery createQuery(EntityManager em, CriteriaQuery criteriaQuery, Path attributePath) { Попытка поиска атрибута для сортировки throwIfNotApplicableFor(attributePath); CriteriaBuilder cb = em.getCriteriaBuilder(); Добавит предложение Path sortPath = attributePath.get(getSortAttribute()); criteriaQuery.orderBy( ORDER BY isSortedAscending() ? cb.asc(sortPath) : cb.desc(sortPath) );
TypedQuery query = em.createQuery(criteriaQuery); query.setFirstResult(getRangeStartInteger());
Установка смещения
if (getSize() != -1) Установка количества результатов query.setMaxResults(getSize());
}
}
return query;
Для постраничной выборки на основе смещений требуется знать номер текущей страницы. По умолчанию текущей является страница 1.
Сортировка и постраничная выборка данных 573 Проверка возможности получения пути к атрибуту сортировки для данной страницы и, следовательно, к используемой запросом модели. Этот метод возбудит исключение, если атрибут сортировки недоступен в модели класса, на который ссылается запрос. Это – механизм повышения надежности, который выдаст осмысленное сообщение об ошибке, если попытаться связать неправильные настройки страницы с неправильным запросом. Добавление в запрос предложения ORDER BY. Установка смещения в запросе: первой выбираемой записи. Установка количества записей, выбираемых для данной страницы.
Мы показали реализацию не всех используемых методов. Например, здесь отсутствует такой метод, как getRangeStartInteger(), который вычисляет номер запи си, первой в данной странице, в зависимости от размера страницы. Этот и другие вспомогательные методы вы найдете в исходном коде. Обратите внимание, что порядок результатов может быть не определен: сортировка выполняется по наименованиям товаров в алфавитном порядке, и несколько товаров имеют одинаковое наименование, база данных вернет их в том порядке, который создатели СУБД посчитали приемлемым. Вы должны сортировать либо по уникальному ключевому атрибуту, либо добавить дополнительный критерий упорядочения по ключевому атрибуту. Несмотря на то что большинство разработчиков просто игнорирует проблему неопределенности при сортировке в методе смещений, предсказуемость порядка сортировки в методе поиска является обязательной.
Реализация метода поиска Для реализации постраничной выборки на основе поиска нужно добавить в запрос ограничения. Предположим, что предыдущая страница показывала товары, отсортированные по наименованиям в алфавитном порядке, до значения «Coffee Machine», как показано на рис. 19.2, и нужно отобразить следующую страницу с помощью запроса SQL. Запомнив последнее значение на предыдущей странице – запись с «Coffe Machine» – и идентификатор (допустим 5), можно написать следующий код SQL: select i.* from ITEM i where i.NAME >= 'Coffee Machine' and ( i.NAME 'Coffee Machine' or i.ID > 5 ) order by i.NAME asc, i.ID asc
Первое ограничение гласит: «Верни все товары, наименование которых больше либо равно [Coffee Machine]», что приведет к поиску вперед до конца предыдущей страницы. База данных может эффективно реализовать данное ограничение с помощью поиска по индексу. Затем накладывается дополнительное ограничение, ис-
574 Создание веб-приложений ключающее товар «Coffee Machine», пропуская таким образом запись, показанную на предыдущей странице. Но в базе данных могут оказаться два товара с наименованием «Coffee Machine». Чтобы данные не потерялись при переходе между страницами, нужно использовать уникальный ключ. Вы должны упорядочить и ограничить результаты, используя этот уникальный ключ. Здесь используется первичный ключ, который гарантирует, что база данных вернет товары, наименование которых не «Coffee Machine», или товары (пусть даже с наименованием «Coffee Machine»), значение идентификатора которых больше показанного на предыдущей странице. Конечно, если наименование товара (или значение другого столбца, по которому производится сортировка) уникально, дополнительный уникальный ключ можно не использовать. Код примера с обобщенным решением предполагает, что всегда будет применяться явный уникальный ключ. Также обратите внимание: тот факт, что идентификаторы товаров представляют собой возрастающую числовую последовательность, не играет никакой роли; самое главное, чтобы ключ гарантировал предсказуемый порядок сортировки. Запрос можно переписать в более компактной форме, используя синтаксис конструктора строк значений: select i.* from ITEM i where (i.NAME, i.ID) > ('Coffee Machine', 5) order by i.NAME asc, i.ID asc
Такое ограничение работает даже с JPQL в Hibernate. Однако в JPA эта возможность не стандартизована; ее нельзя использовать в запросах на основе критериев, и она поддерживается не всеми базами данных. Мы предпочитаем более длинный вариант, работающий везде. Чтобы выполнить сортировку по убыванию, оператор больше, чем следует заменить на меньше, чем. Следующий код реализации класса SeekPage добавляет данное ограничение в запрос на основе критериев. Файл: /apps/app-web/src/main/java/org/jpwh/web/dao/SeekPage.java public class SeekPage extends Page { protected SingularAttribute uniqueAttribute;
Добавление уникального атрибута сортировки
protected Comparable lastValue; Запоминает значения на предыдущей странице protected Comparable lastUniqueValue; // ... @Override public TypedQuery createQuery(EntityManager em, CriteriaQuery criteriaQuery, Path attributePath) {
Сортировка и постраничная выборка данных 575 throwIfNotApplicableFor(attributePath); CriteriaBuilder cb = em.getCriteriaBuilder(); Сортировка результатов Path sortPath = attributePath.get(getSortAttribute()); Path uniqueSortPath = attributePath.get(getUniqueAttribute()); if (isSortedAscending()) { criteriaQuery.orderBy(cb.asc(sortPath), cb.asc(uniqueSortPath)); } else { criteriaQuery.orderBy(cb.desc(sortPath), cb.desc(uniqueSortPath)); }
applySeekRestriction(em, criteriaQuery, attributePath); TypedQuery query = em.createQuery(criteriaQuery);
} }
Добавление ограничений
if (getSize() != -1) Установка количества результатов query.setMaxResults(getSize()); return query;
// ...
В дополнение к обычному атрибуту сортировки методу поиска нужно передать атрибут, представляющий уникальный ключ. Это может быть любой уникальный атрибут модели сущности, но обычно выбирается атрибут первичного ключа. Для обоих атрибутов – сортировки и уникального ключа – нужно запомнить их значения на «предыдущей» странице. После этого можно будет извлечь данные для следую щей страницы, выполнив поиск этих значений. Подойдет любой тип, реализующий интерфейс Comparable, как того требуют запросы на основе критериев. Результаты всегда должны сортироваться по атрибуту сортировки и атрибуту уникального ключа. Нужно добавить дополнительные ограничения (не показанные здесь) в предложение WHERE, чтобы поиск начинался после последних сохраненных значений. Нужно отсечь лишние результаты, согласно размеру страницы.
Целиком метод applySeekRestriction() можно найти в коде примеров; это код запроса на основе критериев, для которого здесь не нашлось места. Итоговый запрос эквивалентен версии SQL, показанной ранее. Давайте протестируем новую функциональность уровня хранения для постраничной выборки. Поиск границ страницы в методе поиска Ранее мы говорили, что будет непросто реализовать в методе поиска переход к конкретной странице, поскольку для конкретной страницы неизвестны последние значения для поиска. Но эти значения можно найти с помощью выражения SQL, как показано ниже:
576 Создание веб-приложений select i.NAME, i.ID from ITEM i where (select count(i2.*) from ITEM i2 where (i2.NAME, i2.ID)