ODk SQL Query Builder

Библиотека с методами построения текстовых sql-запросов в дополнение к любому ORM на Java (17 и выше).

ENG ]


Подключение

Подключение зависимости в maven pom:

<dependency>
    <groupId>io.bitbucket.dsmoons</groupId>
    <artifactId>odk-sql-query-builder</artifactId>
    <version>2.1.0</version>
</dependency>

Новейшая версия и варианты подключения в различных системах сборки доступны на странице поиска в центральном репозитории Maven.


Использование библиотеки

Построение запроса начинается с метода класса QueryObject.

Предикаты запроса (условия операторов WHERE и ON) начинаются с методов класса PredicateObject.

Построение запроса завершается вызовом метода build или toString (работают одинаково) для получения строки запроса.

import static io.bitbucket.dsmoons.odk.sql.query.builder.PredicateObject.field;
import static io.bitbucket.dsmoons.odk.sql.query.builder.QueryObject.select;

Построение запроса

Запрос SELECT

var query = //QueryObject.
        select()
        .from("employees")
        .where(
            //PredicateObject.
                field("FirstName").equalTo("Robert")
                    .and().field("LastName").equalTo("King")
        )
        .build();
SELECT * FROM employees WHERE FirstName = 'Robert' AND LastName = 'King'
var query = select("t1.*")
        .from("table1", "t1")
        .join("table2", "t2")
        .on(
                field("t1.name").equalTo().field("t2.name")
                        .and()
                        .field("t1.age").lessThan().field("t2.age")
        )
        .where(
                field("t1.name").in(
                        select("name").from("table3").where("age > 40")
                )
        )
        .orderBy("t1.age")
        .build();
SELECT t1.* FROM table1 t1 
JOIN table2 t2 ON t1.name = t2.name AND t1.age < t2.age 
WHERE t1.name IN (SELECT name FROM table3 WHERE age > 40) 
ORDER BY t1.age

Запрос DELETE

var query = delete().from(Employees.class).where(field("age").greaterThan(30)).build();
DELETE FROM employees WHERE age > 30

Запрос INSERT

var keys = List.of("name", "age");
var vals1 = List.of("Name1", 30);
var vals2 = List.of("Name2", 31);
var query = insert().into(Employees.class).values(keys, vals1, vals2).build();
INSERT INTO employees (name, age) VALUES ('Name1', 30), ('Name2', 31)
var query = insert().into("employees").values(Map.of("name", "Name1", "age", 30)).build();
INSERT INTO employees (age, name) VALUES (30, 'Name1')

Запрос UPDATE

var query = update(Employees.class).set("name = 'Name'")
        .where(
                brackets(
                        field("age").isNull()
                            .and().field("q1").between(1, 10)
                ).or()
                        .field("q2").greaterThanOrEqualTo(234)
        ).build();
UPDATE employees SET name = 'Name' WHERE (age IS NULL AND q1 BETWEEN 1 AND 10) OR q2 >= 234
var query = update("table").set(Map.of("name", "Name", "age", 30)).where(field("age").isNull()).build();
UPDATE table SET name = 'Name', age = 30 WHERE age IS NULL

Построение предиката

Для сравнения полей используются методы equalTo (=), notEqualTo (<>), greaterThan (>), greaterThanOrEqualTo (>=), lessThan (<), lessThanOrEqualTo (<=).

Сравнение со значением

field("f").equalTo("value")
// f = 'value'

field("f").equalTo(123)
// f = 123

Сравнение с другим полем

field("a.f").equalTo().field("b.f")
// a.f = b.f

Сравнение с результатом вложенного запроса

field("f").equalTo().query(
    select("id").from("table").where(field("age").greaterThan(30))
)
// f = (SELECT id FROM table WHERE age > 30)

Сравнение с выражением (с использованием функций)

field("f").equalTo().expression("NOW() - INTERVAL 1 DAY")
 // f = NOW() - INTERVAL 1 DAY

Проверка на null

field("f").isNull()
// f IS NULL

field("f").isNotNull()
// f IS NOT NULL

Методы in, notIn

field("f").in(1, 2, 3)
// f IN (1, 2, 3)

field("f").in("val1", "val2")
// f IN ('val1', 'val2')

field("f").in(
    select("id").from("table").where(field("age").greaterThan(30))
) 
// f IN (SELECT id FROM table WHERE age > 30) 

Методы between, notBetween

field("f").between(10, 20)
// f BETWEEN 10 AND 20

Методы like, notLike, ilike, notIlike

field("f").like("aaa%")
// f LIKE 'aaa%'

field("f").like("J____n")
// f LIKE 'J____n'

Методы and, or, скобки

field("f").greaterThan(21)
    .or()
    .brackets(
        field("a").lessThan(2)
            .and()
            .field("b").isNull()
    )
// f > 21 OR (a < 2 AND b IS NULL)

Выражение CASE

Построение выражения начинается с методов класса PredicateObject.CaseObject.

import static io.bitbucket.dsmoons.odk.sql.query.builder.PredicateObject.CaseObject.caseExpression;

Простое выражение:

var category = //PredicateObject.CaseObject.
    caseExpression("ProductLine")
         .when("R").then("Road")
         .when("M").then("Mountain")
         .when("T").then("Touring")
         .when("S").then("Other sale items")
         .elseExpression("Not for sale")
         .end("Category");
var categoryQuery = select("ProductNumber", "Name", category).from("Product");
SELECT ProductNumber, Name,
     CASE ProductLine
         WHEN 'R' THEN 'Road'
         WHEN 'M' THEN 'Mountain'
         WHEN 'T' THEN 'Touring'
         WHEN 'S' THEN 'Other sale items'
         ELSE 'Not for sale'
     END AS Category
FROM Product

Поисковое выражение:

var priceRange = caseExpression()
         .when(field("ListPrice").equalTo(0)).then("Item not for resale")
         .when(field("ListPrice").lessThan(50)).then("Under 50")
         .when(field("ListPrice").greaterThanOrEqualTo(50)
                .and().field("ListPrice").lessThan(250)).then("Under 250")
         .when(field("ListPrice").greaterThanOrEqualTo(250)
                .and().field("ListPrice").lessThan(1000)).then("Under 1000")
         .elseExpression("Over 1000")
         .end("PriceRange");
var priceRangeQuery = select("ProductNumber", "Name", priceRange).from("Product");
SELECT ProductNumber, Name,
     CASE
         WHEN ListPrice = 0 THEN 'Item not for resale'
         WHEN ListPrice < 50 THEN 'Under 50'
         WHEN ListPrice >= 50 AND ListPrice < 250 THEN 'Under 250'
         WHEN ListPrice >= 250 AND ListPrice < 1000 THEN 'Under 1000'
         ELSE 'Over 1000'
     END AS PriceRange
FROM Product

Агрегатные функции

Методы агрегатных функций расположены в классе AggregateFunctions. Для создания собственной функции используйте класс Functions.

import static io.bitbucket.dsmoons.odk.sql.query.builder.expressions.AggregateFunctions.count;

var query1 = select(count("*")).from("employees").build();
// SELECT COUNT(*) FROM employees

var query2 = select(count("*", "Count")).from("employees").build();
// SELECT COUNT(*) AS Count FROM employees

Методы build и execute

Для сохранения строки запроса в переменную типа String и дальнейшего использования используется метод build.

String query = select.from("table").where(field("id").equalTo(id)).build();

// промежуточные действия

return sendToDatabase(query);

Для немедленной отправки запроса в БД можно использовать метод execute.

// с использованием ссылки на метод, 
// если он принимает один параметр типа String
return select.from("empl_table").where(field("id").equalTo(id))
                        .execute(this::sendToDatabase);

// или через лямбду, если метод принимает больше параметров
return select.from("empl_table").where(field("id").equalTo(id))
                        .execute(query -> sendToDatabase(query, EmplTable.class));

Переиспользование объектов

Следующие примеры показывают переиспользование объектов Field, Case, Function, Predicate.

Пример №1. Выражение CASE и его псевдоним в одном запросе

import io.bitbucket.dsmoons.odk.sql.query.builder.expressions.Case;
import io.bitbucket.dsmoons.odk.sql.query.builder.predicate.Field;

import static io.bitbucket.dsmoons.odk.sql.query.builder.PredicateObject.CaseObject.caseExpression;
import static io.bitbucket.dsmoons.odk.sql.query.builder.PredicateObject.field;

/**
 * Таблица "customers"
 */
public class Customers {

    private static final Field companyField = field("Company");

    public static final Case companyNameCase = caseExpression().when(companyField.isNotNull()).then(companyField)
            .elseExpression("_No company").end("CompanyName");
}
String query = select("FirstName", "LastName", Customers.companyNameCase)
        .from(Customers.class)
        .orderBy(Customers.companyNameCase.getAlias())
        .build();

Полученный запрос

SELECT FirstName, LastName, 
    CASE 
        WHEN Company IS NOT NULL THEN Company 
        ELSE '_No company' 
    END AS CompanyName 
FROM customers 
ORDER BY CompanyName

Пример №2. Поля двух таблиц в JOIN и других методах

import io.bitbucket.dsmoons.odk.sql.query.builder.expressions.Function;
import io.bitbucket.dsmoons.odk.sql.query.builder.predicate.Field;

import static io.bitbucket.dsmoons.odk.sql.query.builder.PredicateObject.field;
import static io.bitbucket.dsmoons.odk.sql.query.builder.expressions.AggregateFunctions.count;

/**
 * Таблица "artists"
 */
public class Artists {

    public static final String alias = "a";

    public static final Field artistIdField = field("ArtistId", alias);
    public static final Field nameField = field("Name", alias);

    public static final Function albumsCountFunc = count(artistIdField, "AlbumsCount");
}
import io.bitbucket.dsmoons.odk.sql.query.builder.expressions.Function;
import io.bitbucket.dsmoons.odk.sql.query.builder.predicate.Field;

import static io.bitbucket.dsmoons.odk.sql.query.builder.PredicateObject.field;
import static io.bitbucket.dsmoons.odk.sql.query.builder.expressions.AggregateFunctions.groupConcat;

/**
 * Таблица "albums"
 */
public class Albums {

    public static final String alias = "al";

    public static final Field title = field("Title", alias);

    public static final Function titlesFunc = groupConcat(title, ";", "Titles");
}
SelectQuery query = select(Artists.albumsCountFunc, Artists.artistIdField, Artists.nameField, Albums.titlesFunc)
        .from(Artists.class, Artists.alias)
        .join(Albums.class, Albums.alias).using(Artists.artistIdField)
        .groupBy(Artists.artistIdField, Artists.nameField)
        .having(expression(Artists.albumsCountFunc.getAlias()).greaterThan(4))
        .orderBy(Artists.nameField);

Полученный запрос

SELECT COUNT(a.ArtistId) AS AlbumsCount, a.ArtistId, a.Name, GROUP_CONCAT(al.Title, ';') AS Titles 
FROM artists a 
JOIN albums al USING (ArtistId) 
GROUP BY a.ArtistId, a.Name 
HAVING AlbumsCount > 4 
ORDER BY a.Name

Пример №3. Выражение CASE из примера поискового выражения

Field listPrice = field("ListPrice");

Case priceRange = caseExpression()
        .when(listPrice.equalTo(0)).then("Item not for resale")
        .when(listPrice.lessThan(50)).then("Under 50")
        .when(listPrice.greaterThanOrEqualTo(50)
                .and()
                .field(listPrice).lessThan(250)).then("Under 250")
        .when(listPrice.greaterThanOrEqualTo(250)
                .and()
                .field(listPrice).lessThan(1000)).then("Under 1000")
        .elseExpression("Over 1000")
        .end("PriceRange");

Документация в формате javadoc. Пример использования в коде Java

mvnrepository.com/artifact/io.bitbucket.dsmoons/odk-sql-query-builder

История изменений

Релиз 2.1.0 (17.03.2026)

[Join] Добавлены методы join, принимающие подзапрос. Удалены устаревшие методы.

[Field] Добавлен метод doubleColon.


Патч 2.0.2 (02.03.2026)

[PredicateObject]: Добавлен метод field, принимающий объект Field.


Патч 2.0.1 (03.02.2026)

[QueryObject]: Исправлена ошибка в методе select.


Релиз 2.0.0 (02.02.2026)

Проект переписан на язык Java.

[Join]: Объявлены устаревшими методы leftJoin, rightJoin, fullOuterJoin. Добавлена группа методов join, принимающие тип join.


Релиз 1.8.0 (13.01.2026)

[Field]: Добавлены методы ilike, notIlike.


Релиз 1.7.0 (06.05.2025)

[Join]: Исправлены ошибки в методах crossJoin, naturalJoin.

[SelectQuery]: Добавлены методы whereExists, whereNotExists.


Патч 1.6.1 (31.03.2025)

[InsertQuery.Values]: Исправлена ошибка в методе values(Map).


Релиз 1.6.0 (22.03.2025)

[Join.OnCondition]: Добавлен метод using, принимающий KCallable.

[InsertQuery.Values]: Добавлены методы select для построения выражения INSERT INTO SELECT.

[PredicateObject.CaseObject]: Добавлен метод caseExpression, принимающий Field.

Добавлен класс Operators.


Релиз 1.5.0 (15.03.2025)

Класс Predicate переименован в Builder.

Класс PredicateOperator переименован в Predicate.

Класс PredicateField переименован в Expression.

Класс PredicateCondition переименован в Field.


Добавлен класс AggregateFunctions с методами создания агрегатных функций.

Добавлен класс Function, с помощью которого можно создавать пользовательские агрегатные функции.


[Join.OnCondition]: Добавлен метод using, принимающий Field.

[PredicateObject]: Добавлены методы not, expression, value.

[Predicate.Builder]: Добавлены методы not, expression, value.

[Predicate.Expression]: Добавлены методы any, all, field.

[SelectQuery]: Добавлены методы orderBy и orderByDesc, принимающие Field, метод having, принимающий Predicate.


[Case]: Метод build объявлен устаревшим. Вместо него при необходимости используется метод toString.

[Predicate.Expression]: Метод caseExpression объявлен устаревшим. Вместо него используется метод expression.


Релиз 1.4.0 (03.03.2025)

[AbstractQuery]: Добавлен метод execute для передачи строки запроса в БД и получения ответа через лямбду.

[From]: Добавлен метод from, принимающий подзапрос SelectQuery.

[Predicate.PredicateField]: Добавлен метод caseExpression.

Добавлены классы PredicateObject.CaseObject, Predicate.Case с методами построения выражений CASE.


Релиз 1.3.0 (13.02.2025)

[Predicate.PredicateCondition]: Добавлены методы notBetween, notLike, notIn, метод in, принимающий список (list) значений.

[Predicate.PredicateField]: Добавлен метод expression.

[SelectQuery]: Методу limit добавлен необязательный параметр offset.

Исправлены комментарии к некоторым методам.


Релиз 1.2.0 (28.01.2025)

[Predicate.PredicateField]: Добавлен метод query.

[Where]: Исправлены опечатки в комментариях.

[QueryObject]: Методам update добавлен необязательный параметр alias.


Патч 1.1.1 (22.01.2025)

Удалена лишняя зависимость.


Релиз 1.1.0 (17.01.2025)

[QueryObject]: Добавлены перегрузки метода delete, принимающие Class и KClass.

[AbstractQuery]: Добавлен метод build, возвращающий строку запроса (работает аналогично методу toString).


Патч 1.0.1 (10.01.2025)

[PredacateCondition]: Исправлена ошибка в методе in.


Релиз 1.0.0 (09.01.2025)

Первый релиз.

⊟ Скрыть