Борьба с native queries в EclipseLink 2.5 (Oracle DB)
Предыстория: у меня был проект – веб-приложение, работающее в GlassFish. Приложение собиралось в EAR а слой данных был реализован на базе JPA
. В качестве реализации JPA использовался EclipseLink 2.5
, встроенный в GlassFish 4.0
. База данных – Oracle 11g Express Edition.
Вообще, до этого я всегда работал с Hibernate, и EclipseLink заставил немного помучаться с особенностями восприятия JPQL. Например, EclipseLink не хотел понимать count(*)
и все используемые таблицы обязывал снабжать алиасами. Но это мелочи, всё было хорошо, пока не понадобилось сделать несложный запрос: получить объекты с условием, в котором было необходимо применение арифметики с датами. Итак, краткая история мучений.
JPQL: OPERATOR()
Вычитал, что в EclipseLink JPQL можно использовать оператор OPERATOR('AddDate')
тынц. Как оказалось, в EclipseLink этот оператор реализован не был. Что в принципе удивительно, т.к. вендором EclipseLink сейчас является Oracle, и нормальный человек в этом случае имеет все основания ожидать хорошей поддержки их СУБД.
JPQL: SQL()
Попробовал также и SQL()
, позволяющий вставлять в JPQL вставки нативного SQL тынц. К сожалению, и тут не получилось. Результирующий SQL был с багом, говорящем о несоответствии количества открывающих и закрывающих скобок.
Пробуем @NamedNativeQuery
Отчаявшись решить проблему в рамках JPQL, решил спуститься на ступеньку ниже и заюзать Native Query
. Но запрос мой требовал соединения четырёх сущностей, и некоторые поля в них назывались по умолчанию одинаково (ID
). Аннотация @EntityResult
позволяет для таких ситуаций задать discriminatorColumn
, но мне не удалось заставить его работать. Маппинг работал некорректно, значения пропертей маппились некорректно в любом варианте использования. Более того, выяснилось, что все столбцы должны иметь уникальный префикс. Иначе вложенные объекты могут захватить свойство (например, ID
) внешнего объекта, чей столбец ID
был без префикса. Всё это выглядит настолько бажным, что становится странным, как такое количество багов может считаться приемлемым.
Работающий маппинг теперь имел вид:
@SqlResultSetMapping( name = "request-department", entities = { @EntityResult( entityClass = Request.class, fields = { @FieldResult( name = "id", column = "R_ID"), @FieldResult( name = "deadlineDate", column = "R_DEADLINE_DATE"), @FieldResult( name = "correctedDeadlineDate", column = "R_CORRECTED_DEADLINE_DATE") }), @EntityResult( entityClass = Department.class, fields = { @FieldResult( name = "id", column = "DEP_ID"), @FieldResult( name = "description", column = "DEP_DESCRIPTION"), @FieldResult( name = "name", column = "DEP_NAME") }), @EntityResult( entityClass = User.class, fields = { @FieldResult( name = "id", column = "U_ID"), @FieldResult( name = "email", column = "U_EMAIL"), @FieldResult( name = "personalFirstName", column = "U_FIRST_NAME"), @FieldResult( name = "personalLastName", column = "U_LAST_NAME"), @FieldResult( name = "personalMiddleName", column = "U_MIDDLE_NAME") }), @EntityResult( entityClass = User.class, fields = { @FieldResult( name = "id", column = "ASS_ID"), @FieldResult( name = "email", column = "ASS_EMAIL"), @FieldResult( name = "personalFirstName", column = "ASS_FIRST_NAME"), @FieldResult( name = "personalLastName", column = "ASS_LAST_NAME"), @FieldResult( name = "personalMiddleName", column = "ASS_MIDDLE_NAME") }) }) |
Left join и вложенные объекты
Тут я вспомнил, что мне на самом деле нужен не INNER JOIN
, а LEFT JOIN
. То есть мне нужно сделать один из @EntityResult
– nullable. Но этого сделать нельзя ! EclipseLink видит, что User.Id
– первичный ключ, и падает на утверждении, что он не может быть Null
. Ничего нельзя сделать, кроме того, что подставлять в столбец магическое число, сигнализирующее о том, что на самом деле сущность отсутствует. Вложенные объекты, кстати, тоже подцепить не удаётся. Но можно их зацепить отдельно, а потом вручную присвоить свойствам внешнего объекта.
http://www.eclipse.org/forums/index.php/t/305321/ https://www.java.net/node/675607
Параметры
Ок, после явного прописывания всех столбцов и использования -1 для сигнализации null-объектов, все работало. Теперь я подумал, что неплохо бы добавить в мой Named Native Query параметры. Как оказалось, это невозможно. Параметры в named native queries не работают вообще и никак. Не работает ничего: ни именование с двоеточия, ни вопросиками, ни через решётку (как предлагают в одном из ответов).
http://eclipse.1072660.n5.nabble.com/Params-on-NamedNativeQuery-td3401.html
Пришлось убрать аннотацию @NamedNativeQuery
, и сделать всё руками:
Query nativeQuery = entityManager.createNativeQuery( String.format( sql, daysBeforeDeadline, RequestStatus.Sent.ordinal(), RequestStatus.Processing.ordinal() ), "request-department" ); List<Object[]> resultList = nativeQuery.getResultList(); for ( Object[] rowObjects : resultList ) { Request request = ( Request ) rowObjects[0]; Department department = ( Department ) rowObjects[1]; User user = ( User ) rowObjects[2]; User assignee = (User) rowObjects[3]; request.setDepartment( department ); request.setUser( user ); if (assignee.getId() != -1) request.setAssignee( assignee ); requests.add( request ); } return requests; |
Прикол в том, что и здесь параметры тоже не работают ! Приходится вставлять их с помощью String.format()
.
(Тут вроде у мужиков работает, но у меня и так не заработало)
Заключение
В общем, я рад, что таки-получилось заставить это работать, но какой ценой ? Программистам EclipseLink стоит задуматься о качестве своего продукта, если на такие простые вещи за столько лет разработки у них не отработаны работающие сценарии на одной из самых популярных СУБД, тем более что они теперь – вендоры этой ORM. Но если вы желаете использовать в своём проекте native queries, то лучше придерживаться следующих правил:
- Не использовать @NamedNativeQuery
- Явно задавать все имена столбцы, они должны быть уникальны и не должны совпадать по названию ни с одним из свойств вложенных сущностей. Лучше всего задать уникальные префиксы для каждой сущности.
- Там, где ожидаются nullable-столбцы, но которые мапятся на свойства, не допускающие значений
null
, придётся использовать магические константы или другие столбцы в качестве флагов - Параметры не работают – придётся вставлять их вручную
0