Борьба с native queries в EclipseLink 2.5 (Oracle DB)

Written by elwood

JPA Logo

Предыстория: у меня был проект – веб-приложение, работающее в 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, придётся использовать магические константы или другие столбцы в качестве флагов
  • Параметры не работают – придётся вставлять их вручную