Недавно, в поисках золотой середины между JDBC и ORM, я натолкнулся на интересную open source библиотеку (лицензия Apache Software License), с помощью которой можно строить SQL прямо в Java-коде достаточно удобно и безопасно. Библиотека называется Jooq. Jooq включает в себя генератор кода, который парсит структуру вашей базы данных и создает необходимые Java-классы. На деле получается примерно такой код:
Как видите, конструирование запроса и его выполнение для простых типов занимает одну строку. Немного о jooq:
Впрочем, мнения на этот счет могут быть разными. Кому-то такой код покажется более понятным.
Возможно, примеры лучше глянуть тут с подсветкой синтаксиса от Geshi.
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
- Querydsl
- SQL Construction Kit (документации по нему найти не удалось)
Еще аналоги, несколько иного плана
Контакты:
- Сайт проекта — http://www.jooq.org
- Мануал: http://sourceforge.net/apps/trac/jooq/wiki/Manual
- Форум — http://groups.google.com/group/jooq-user
- SVN Info: https://sourceforge.net/scm/?type=svn&group_id=283484
Несколько примеров из исходников Jooq (работа с базой information_schema в MySQL)
Возможно, примеры лучше глянуть тут с подсветкой синтаксиса от Geshi.
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);
}
}