Category Archives: SQL
Наткнулся на прекрасный онлайн-квест https://www.alexbers.com/sql/. Прошёл его и очень всем рекомендую – я, например, хоть и знал, чем опасны SQL-инъекции, но вживую ни разу инъекцию не раскручивал. А тут как раз самое нужное – практические навыки !
После выполнения квеста в мозгу складывается довольно стройная картина. И вывод таков: при наличии уязвимости и самого маленького output’а на сайте (кол-во записей, или время выполнения запроса) из инъекции можно вытащить вообще всю базу целиком ! Грубо говоря, обладая одним битом информации и либерально настроенным mysql сервером, можно получить названия баз, таблиц, колонок и значений вообще всего 🙂
Кстати, кажется, до 5ой версии MySql это было невозможно, и приходилось угадывать и перебирать имена таблиц-колонок. Но теперь..
В общем, попробуйте, не пожалеете. За подсказками можно сходить на хабр : http://habrahabr.ru/post/253885/. Хотя там 8 и 9 решены кривовато – но в комментариях описан и “правильный” путь.
Предыстория: у меня был проект – веб-приложение, работающее в 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
, придётся использовать магические константы или другие столбцы в качестве флагов - Параметры не работают – придётся вставлять их вручную
На днях вспомнил об одной задаче, которая мне попалась на работе года полтора назад.
В то время я еще не знал об sql-ex.ru и мои познания в SQL были достаточно слабыми, на уровне
простейших селектов с inner join’ами. И, как я помню тогда, я не смог решить эту задачу без
дополнительного подзапроса. Сейчас, после нескольких десятков решенных задач на sql-ex.ru я
без проблем с ней справился, что навело меня на мысль о том, что это – неплохой тест на знание
азов SQL. В отличие от более сложных задач, эта задачка не требует большого количества времени, но
покрывает почти всё, что требуется для среднего программера в вопросе знания SQL, особенно если решать её в уме. Сразу отпадают вопросы наподобие “что такое JOIN, чем LEFT JOIN отличается от INNER JOIN, как работает группировка” итд.
Итак, формулировка такова. Есть 2 таблицы – Collection и Item.
Необходимо сделать выборку
Collection.ID Collection.Name Collection.Count
тех коллекций, которые содержат меньше 5 элементов.
Вот и всё.
ДАЛЬШЕ СПОЙЛЕР, НЕ ЧИТАТЬ !!!111 (Я не научился пока скрывать в вордпрессе куски постов, но как только научусь – оформлю соответствующе).
Алгоритм решения задачи.
Ну ясно же – надо сделать JOIN, делаем JOIN, группируем по Collection.ID, выводим ID, Name, Count(*).
Черт. При группировке по ID нельзя вывести Name. Значит, надо либо завернуть полученную выборку в еще 1 запрос, либо дописать в группировку этот Name. Ок, работает. Только пустые коллекции не выводятся. Почему ? Ну ясно же – они не попадают в результат INNER JOIN’a, надо использовать LEFT JOIN. О, теперь получилось. Только коллекции-то пустые, а count(*) выводит единицу вместо нуля. Ах да, это же LEFT JOIN, он джойнит Collection.ID Collection.Name NULL NULL и count(*) дает единицу. Как поправить-то. Вычесть 1 нельзя, поскольку так можно запороть непустые коллекции. Надо как-то проверить, является ли коллекция пустой, и для нее вывести 0, а для остальных – count(*). Да у нас же справа NULL, можно их в какую-нибудь агрегатную функцию запихнуть, и сравнить с NULL’ом. Если NULL, значит, коллекция пустая, и для нее выводим 0, иначе – count(*). Эврика! Ну и добавляем HAVING. Пишем запрос:
SELECT c.ID, c.Name, CASE WHEN COUNT(*) = 1 AND MAX(i.CollectionID) IS NULL THEN 0 ELSE COUNT(*) END FROM Collection c LEFT JOIN Item i ON c.ID = i.CollectionID GROUP BY c.ID, c.Name HAVING COUNT(*) < 5 |
0