Category Archives: Java
Предыстория: у меня был проект – веб-приложение, работающее в 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
, придётся использовать магические константы или другие столбцы в качестве флагов - Параметры не работают – придётся вставлять их вручную
Часто бывает нужно передать данные с серверной части на клиентскую, причём не в виде текста, а в виде готовых объектов. К сожалению, в 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 ); } } } |
Часто бывает нужно убить процесс 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
Будут завершены все подходящие процессы без запроса подтверждений.
0