22 Апр 2011

Jooq — «LINQ» для Java, типобезопасный построитель SQL запросов в Java коде

Category: JavaFractalizeR @ 14:26

Недавно, в поисках золотой середины между JDBC и ORM, я натолкнулся на интересную open source библиотеку (лицензия Apache Software License), с помощью которой можно строить SQL прямо в Java-коде достаточно удобно и безопасно. Библиотека называется jooq. jooq включает в себя генератор кода, который парсит структуру вашей базы данных и создает необходимые Java-классы. На деле получается примерно такой код:

Integer taskId = sqlFactory.select(ID).from(TASK).where(STATUS.equal(TaskStatus.QUEUED)).
    orderBy(LAST_UPDATED).limit(1).fetchOne(ID);

Как видите, конструирование запроса и его выполнение для простых типов занимает одну строку. Немного о jooq:

Что у нас хорошего?

  • Поддержка MySQL 5.1.41 / 5.5.8, Oracle XE 10.2.0.1.0, DB2 9.7, PostGreSQL 9.0, H2 1.3.154, HSQLDB 2.1.0, SQLite, Derby 10.7
  • Планируется поддержка Sybase, MSSQL, Ingres, Firebird
  • Поддержка генерации кода не только для таблиц / полей, но и представлений, хранимых процедур, UDF, сложных типов вроде ENUM в MySQL, Blob (можно представить их в виде обычных byte[])
  • Поскольку вся схема базы данных представлена классами, в нормальных IDE (и даже в тех, что не очень) 😉 работает автозавершение языковых конструкций
  • Отсутствие уязвимости SQL Injection при правильном использовании — jooq пользуется шаблонами параметров в запросах
  • По большей части независимый от базы данных синтаксис
  • Использование обобщенных типов обеспечивает достаточное количество проверок еще при компиляции кода. Плюс невозможно забыть исправить какой-либо кусочек кода, если вы вдруг переименовали столбец в таблице БД. После повторного парсинга БД некорректный код просто не скомпилируется
  • Базовая поддержка ActiveRecord (полученную из базы данных запись можно изменить и записать назад одним-двумя строчками)
  • Поддержка не только SELECT запросов, но и INSERT, UPDATE.
  • Поддержка вложенных запросов, произвольных полей, агрегатных функций, UNION запросов, арифметических и других операций
  • Удобные методы получения результатов выполнения запроса — fetch(), fetchAny(), fetchLazy(), fetchMap() и другие.
  • Хорошая поддержка SLF4J в том числе и для профайлинга внутреннего устройства Jooq.
  • Много чего запланировано
  • Автор постоянно в интернете, очень оперативно отвечает на вопросы в jooq Google Group и на тикеты в баг-трекере.
  • Поддержка Maven. Дистрибутивы jooq доступны через Maven Central.
  • Поддержка как DSL синтаксиса (select().from().where()) конструирования запросов, так и ООП (a=new Query(); a.addSelect(); a.addFrom())
  • Результаты запросов и сами объекты запросов сериализуемы.

Что у нас плохого?

  • Пока нет непосредственной поддержки некоторых специфичных языковых возможностей вроде «FOR UPDATE» или «index usage hints»конструкций в SELECT. Но есть workaround.
  • Если вы работаете в одном классе более чем с одной таблицей, import static в некоторых случаях использовать затруднительно, что приводит к некоторому усложнению внешнего вида запроса (пример по отношению к тому, что указан сверху):
Integer taskId = sqlFactory.select(Task.ID).from(Task.TASK).where(Task.STATUS.equal(TaskStatus.QUEUED)).
    orderBy(Task.LAST_UPDATED).limit(1).fetchOne(Task.ID);
  • Небольшой оверхед при исполнении запросов и обходе результата (думаю, он небольшой, десятки-сотни микросекунд, может быть?)
  • Пока нет непосредственной поддержки LAST_INSERT_ID() в MySQL. Но есть workaround. Sequences поддерживаются.

 

Субъективности

  • Кода пока нет на GitHub ;), основная разработка ведется в Subversion
  • SourceForge страницы проекта слегка тормозят, что немного раздражает.
  • Мануал вроде бы достаточно подробен, но некоторые простые, но полезные вещи в нем отсутствуют. Он также показался мне не слишком удобным. В нем описано только самое основное без «фишек», «плюшек» и удобностей. Впрочем, если у вас большой стаж Java, наверное, до всего можно и самому быстро дойти. Кроме того, мне показалось, что он не слишком хорошо структурирован. Информация по нему размазана. Я всегда тороплюсь и хороший мануал для меня must-have…
  • Не слишком удобный синтаксис для работы с агрегированными полями (во всяком случае, я по-другому пока не придумал, как можно это сделать удобно)
Field<Integer> jobTypeCountField = Job.JOBTYPE_ID.count().as("JOBTYPE_ID_COUNT");
 
Result<Record> jobTypeCountRecord = null;
jobTypeCountRecord = sqlFactory.select(Job.JOBTYPE_ID, jobTypeCountField).from(Job.JOB)
    .where(Job.STATUS.equal(JobStatus.EXECUTING)).groupBy(Job.JOBTYPE_ID).fetch();
for (Record record : jobTypeCountRecord) {
    System.out.println(record.getValue(Job.JOBTYPE_ID) + " - " - record.getValue(jobTypeCountField));
}

Впрочем, мнения на этот счет могут быть разными. Кому-то такой код покажется более понятным.

  • В версии 2.0 запланированы очень интересные плюшки.
  • Не-wiki документация (Trac) и не слишком удобная по ней навигация. Впрочем, в коде достаточно javadoc чтобы все понимать по Ctrl-Q. Впрочем, просто я считаю Trac не слишком удобным, вот и придираюсь…
  • Пока сравнительно небольшое количество пользователей.

Аналоги jooq

 

Контакты:

 

Несколько примеров из исходников Jooq (работа с базой information_schema в MySQL)

select(KeyColumnUsage.CONSTRAINT_NAME, KeyColumnUsage.TABLE_NAME, KeyColumnUsage.COLUMN_NAME)
                .from(KEY_COLUMN_USAGE).join(TABLE_CONSTRAINTS)
                .on(KeyColumnUsage.TABLE_SCHEMA.equal(TableConstraints.TABLE_SCHEMA))
                .and(KeyColumnUsage.TABLE_NAME.equal(TableConstraints.TABLE_NAME))
                .and(KeyColumnUsage.CONSTRAINT_NAME.equal(TableConstraints.CONSTRAINT_NAME))
                .where(TableConstraints.CONSTRAINT_TYPE.equal(constraintType))
                .and(KeyColumnUsage.TABLE_SCHEMA.equal(getSchemaName()))
                .orderBy(KeyColumnUsage.TABLE_NAME.ascending(), KeyColumnUsage.ORDINAL_POSITION.ascending()).fetch()
for (Record record : create().select(
                    ReferentialConstraints.CONSTRAINT_NAME,
                    ReferentialConstraints.TABLE_NAME,
                    ReferentialConstraints.REFERENCED_TABLE_NAME,
                    ReferentialConstraints.UNIQUE_CONSTRAINT_NAME,
                    KeyColumnUsage.COLUMN_NAME)
                .from(REFERENTIAL_CONSTRAINTS)
                .join(KEY_COLUMN_USAGE)
                .on(ReferentialConstraints.CONSTRAINT_SCHEMA.equal(KeyColumnUsage.CONSTRAINT_SCHEMA))
                .and(ReferentialConstraints.CONSTRAINT_NAME.equal(KeyColumnUsage.CONSTRAINT_NAME))
                .where(ReferentialConstraints.CONSTRAINT_SCHEMA.equal(getSchemaName()))
                .orderBy(
                    KeyColumnUsage.CONSTRAINT_NAME.ascending(),
                    KeyColumnUsage.ORDINAL_POSITION.ascending())
                .fetch()) {
 
            String foreignKey = record.getValue(ReferentialConstraints.CONSTRAINT_NAME);
            String foreignKeyColumn = record.getValue(KeyColumnUsage.COLUMN_NAME);
            String foreignKeyTableName = record.getValue(ReferentialConstraints.TABLE_NAME);
            String referencedKey = record.getValue(ReferentialConstraints.UNIQUE_CONSTRAINT_NAME);
            String referencedTableName = record.getValue(ReferentialConstraints.REFERENCED_TABLE_NAME);
 
            TableDefinition foreignKeyTable = getTable(foreignKeyTableName);
 
            if (foreignKeyTable != null) {
                ColumnDefinition column = foreignKeyTable.getColumn(foreignKeyColumn);
 
                String key = getKeyName(referencedTableName, referencedKey);
                relations.addForeignKey(foreignKey, key, column);
            }
        }

Метки: , ,

Ответить

Для отправки комментария вам нужно зарегистрироваться. Войти.