Category Archives: Java

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

Внедряем данные из spring model в js-код

Written by elwood

Часто бывает нужно передать данные с серверной части на клиентскую, причём не в виде текста, а в виде готовых объектов. К сожалению, в HTML-страницу нельзя внедрить данные напрямую, но можно сгенерировать js-скрипт, в который бы передавался JSON, готовый к употреблению. Обычно я делал это вручную, но теперь решил написать спринговый interceptor, который бы перехватывал все атрибуты model, начинающиеся с “js_”, и добавлял их на клиент автоматически.

Пример использования

@RequestMapping(value = "/create", method = RequestMethod.GET)
public String viewCreate(Model model) {
	NewsArticle article = new NewsArticle();
        // Этот атрибут будет доступен только в JSP при генерации страницы
	model.addAttribute( "article", article );
	// А этот атрибут также попадёт и в js-код в качестве атрибута глобального объекта JS_DATA
	model.addAttribute( "js_article_id", article.getId() );
	return "edit";
}
$(function() {
    var articleId = JS_DATA['js_article_id'];
    // ...
});

Код

Все необходимые кусочки файлов можно посмотреть на github, а здесь приведу только код интерсептора:

public class JsDataInjectionInterceptor extends HandlerInterceptorAdapter
{
    private final static Log log = LogFactory.getLog( JsDataInjectionInterceptor.class );
 
    @Override
    public void postHandle( HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler,
                            ModelAndView modelAndView ) throws Exception {
        //
        if (null == modelAndView) return;
 
        Map<String, Object> jsObjects = null;
        for ( Map.Entry<String, Object> entry : modelAndView.getModelMap().entrySet() ) {
            if (entry.getKey().startsWith( "js_" )) {
                if (null == jsObjects)
                    jsObjects = new HashMap<>(  );
                jsObjects.put( entry.getKey(), entry.getValue() );
            }
        }
        if (jsObjects != null) {
            ObjectMapper mapper = new ObjectMapper();
            mapper.enable( SerializationFeature.INDENT_OUTPUT );
            String jsData = mapper.writeValueAsString( jsObjects );
            log.debug( "JsData: " + jsData );
            modelAndView.getModelMap().addAttribute( "__js_data__",
                    jsData
            );
        }
    }
}

pkill для java-процессов на Windows

Written by elwood

batch-file

Часто бывает нужно убить процесс java, но именно тот, который был запущен с определёнными параметрами (т.к. в системе могут быть запущено несколько java-процессов). Для этого в unix-мире есть удобная команда pkill, которая выполняет то же самое, что и команда

ps | grep glassfish | awk '{print $2}' | xargs kill

А в Windows таких удобных команд нет. Стандартная команда tasklist не умеет выводить аргументы, с которыми были запущены процессы. Зато есть входящая в JDK утилита jps, которая умеет это делать для java-процессов. Нет grep, но есть FindStr. Нет возможности распарсить вывод с помощью awk, но есть FOR /F. Итак, несмотря на скудный инструментарий, мы всё же что-то можем сделать.

Примеры использования (Нужно предварительно добавить JDK\bin в PATH):

javakill glassfish

Находит java-процесс, в JVM-опциях содержащий строку glassfish, и убивает его, если он такой один. Если же найдено несколько подходящих процессов, будет запрошено подтверждение завершения первого из них.

javakill glassfish /Q

То же самое, но в случае неоднозначности первый подходящий процесс будет завершён без запроса подтверждения.

javakill glassfish /A

Если будут найдены несколько подходящих процессов, будет запрошено подтверждение на завершение их всех.

javakill glassfish /Q /A

Будут завершены все подходящие процессы без запроса подтверждений.