TL;DR;
Мы хотели реализовать пагинацию, и для этого нам пришлось форкнуть диалект для Hibernate.
В тот день ничего не предвещало еды беды увлекательных приключений. Всё началось с сообщения в файлообменнике Skype.
“Мы будем делать новый проект!” - говорили они. “Там всё просто.” - говорили они.
Наши приключения начались в тот момент, когда мы узнали, что у заказчика есть определенный стек, в который требуется вписать новый проект. Ядро стека составляли известные слова на букву “ха”: Hadoop, HBase, Hive. Добавьте сюда немного Kafka и так любимых в мире enterprise “гибких и готовых к использованию” интеграционных решений.
Начало
Итак, нашей команде предстояло реализовать проект на технологиях, которые мы впервые видим, но, ничего, мы же эксперты. Нам нужно было изучить две NoSQL СУБД (Hive и HBase) и выбрать наиболее подходящее. В итоге остановились на HBase + Apache Phoenix. Почему:
Hive оказался медленным для наших задач
Hive поддерживал транзакционность очень слабо
HBase быстр, но пользоваться им напрямую нам показалось неудобным
Apache Phoenix добавлял SQL интерфейс к NoSQL (HBase)
Apache Phoenix поддерживал транзакционность
Расплата
Самое основное:
Apache Phoenix не поддерживает создание view на несколько таблиц ಠ_ಠ
Нет ограничений по уникальности (кроме первичного ключа)
Внешние ключи тоже не поддерживает ¯\_(ツ)_/¯
При чем тут Hibernate
Мы решили пойти легким (как нам тогда казалось) путем и использовать стандартный подход для работы с базой данных: Spring Data / Hibernate диалект для СУБД. Найти диалект для Apache Phoenix оказалось подозрительно легко. Jorge Ruesga разработал диалект и опубликовал его на Github. Мы взяли диалект и пошли фигачить CRUD запросы.
Работа на полчаса
Сначала всё шло хорошо. А хорошо - потому что мы брали по одной сущности или сразу все сущности из таблицы. Сюрприз поджидал нас в один из пятничных вечеров. Ну знаете, в такие вечера, в которые ты уже более менее расслабился и думаешь, что скоро спокойно пойдешь домой, потому что тут работы на полчаса.
Всё началось с того, что мы захотели доставать не все данные из таблицы, а часть. Короче говоря, реализовать стандартную пагинацию.
“Ну что может быть сложного?” - спросите вы. Вот и мы так думали. Просто берешь и делаешь что-то типа такого:
public interface UserRepository extends CrudRepository<User, Integer> {
Page<User> find(Pageable pageable);
}
И всё должно работать. Ну ведь да? Все мы знаем, что под капотом этого метода формируется запрос с OFFSET и LIMIT. Но что, если я вам скажу, что в итоговом запросе OFFSET и LIMIT перепутаны местами?
True изменения
Как и все другие интересные истории про задачи “на полчаса” наша задача казалась нам какой-то мелочью, которую можно поправить “на раз-два взяли”. Скажу лишь, что в итоге мы потратили на нее почти пять часов, прежде чем до нас дошло, что что-то идет не так.
Оказалось, что чудесный Hibernate использует специальный класс при формировании конструкции с OFFSET и LIMIT: LimitHandler. А наш не менее чудесный диалект реализует этот класс таким образом, что в итоге значения для OFFSET и LIMIT оказываются перепутаны.
В итоге, чтобы это поправить нам пришлось форкнуть этот диалект, дописав в него всего несколько строк:
@Override
public boolean bindLimitParametersInReverseOrder() {
return true;
}
Итог
Мы хотели реализовать пагинацию и для этого нам пришлось форкнуть диалект для Hibernate.