Статьи

Любопытный случай с недостатком дизайна jOOQ API

jOOQ — это внутренний предметно-ориентированный язык (DSL) , моделирующий язык SQL (внешний DSL) в Java (основной язык). Основной механизм API jOOQ описан в этой популярной статье:

Ускоренный курс по разработке API Java Fluent .

Любой может реализовать внутренний DSL на Java (или на большинстве других основных языков) в соответствии с правилами из этой статьи.

Пример функции языка SQL: BOOLEANs

Однако одна из приятных особенностей языка SQL — это тип BOOLEAN , который был введен в язык позднее, начиная с SQL: 1999 . Конечно, без логических значений вы можете просто смоделировать значения TRUE и FALSE через 1 и 0 и преобразовать предикаты в значение, используя CASE

1
CASE WHEN A = B THEN 1 ELSE 0 END

Но с истинной поддержкой BOOLEAN вы можете выполнять удивительные запросы, такие как следующий запрос PostgreSQL, который выполняется к базе данных Sakila :

1
2
3
4
5
6
7
8
SELECT
  f.title,
  string_agg(a.first_name, ', ') AS actors
FROM film AS f
JOIN film_actor AS fa USING (film_id)
JOIN actor AS a USING (actor_id)
GROUP BY film_id
HAVING every(a.first_name LIKE '%A%')

Вышеуказанные выходы:

1
2
3
4
5
6
7
8
TITLE                    ACTORS
-----------------------------------------------------
AMISTAD MIDSUMMER        CARY, DARYL, SCARLETT, SALMA
ANNIE IDENTITY           CATE, ADAM, GRETA
ANTHEM LUKE              MILLA, OPRAH
ARSENIC INDEPENDENCE     RITA, CUBA, OPRAH
BIRD INDEPENDENCE        FAY, JAYNE
...

Другими словами, мы ищем все фильмы, в которых все актеры, сыгравшие в фильме, содержат букву «А» в своих именах. Это делается с помощью агрегации по логическому выражению / предикату first_name LIKE '%A%' :

1
HAVING every(a.first_name LIKE '%A%')

Теперь, с точки зрения API jOOQ, это означает, что нам придется предоставлять перегрузки метода have having() которые принимают разные типы аргументов, такие как:

1
2
3
4
5
6
// These accept "classic" predicates
having(Condition... conditions);
having(Collection<? extends Condition> conditions);
 
// These accept a BOOLEAN type
having(Field<Boolean> condition);

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

Как упоминалось ранее, начиная с SQL: 1999, Condition и Field<Boolean> jOOQ Field<Boolean> — это одно и то же. jOOQ позволяет конвертировать между ними через явный API:

1
2
3
Condition condition1 = FIRST_NAME.like("%A%");
Field<Boolean> field = field(condition1);
Condition condition2 = condition(field);

… А перегрузки делают преобразование более удобным для использования.

Так в чем проблема?

Проблема в том, что мы подумали, что было бы неплохо добавить еще одну удобную перегрузку — метод having(Boolean) метод, в котором для удобства могут быть введены постоянные, обнуляемые значения BOOLEAN в запросе, что может быть полезно при построении динамического SQL. или комментируя некоторые предикаты:

1
2
3
4
5
6
7
8
DSL.using(configuration)
   .select()
   .from(TABLE)
   .where(true)
// .and(predicate1)
   .and(predicate2)
// .and(predicate3)
   .fetch();

Идея заключается в том, что ключевое слово WHERE никогда не будет закомментировано, независимо от того, какой предикат вы хотите временно удалить.

К сожалению, добавление этой перегрузки создало неудобства для разработчиков, использующих автозаполнение IDE. Рассмотрим следующие два вызова метода:

1
2
3
4
5
6
// Using jOOQ API
Condition condition1 = FIRST_NAME.eq   ("ADAM");
Condition condition2 = FIRST_NAME.equal("ADAM");
 
// Using Object.equals (accident)
boolean = FIRST_NAME.equals("ADAM");

(Случайно) добавив букву «s» в метод equal() — в основном из-за автодополнения IDE — все выражение предиката резко меняет семантику, начиная с элемента дерева выражений jOOQ, который можно использовать для генерации SQL, до «обычного» логического значения значение (которое всегда дает false , очевидно).

До добавления последней перегрузки это не было проблемой. Использование метода equals() не будет компилироваться, поскольку не было применимой перегрузки, принимающей boolean тип Java.

01
02
03
04
05
06
07
08
09
10
// These accept "classic" predicates
having(Condition condition);
having(Condition... conditions);
having(Collection<? extends Condition> conditions);
 
// These accept a BOOLEAN type
having(Field<Boolean> condition);
 
// This method didn't exist prior to jOOQ 3.7
// having(Boolean condition);

После jOOQ 3.7 эта авария стала незаметно в пользовательском коде, так как компилятор больше не жаловался, что привело к неправильному SQL.

Вывод: будьте осторожны при разработке внутреннего DSL. Вы наследуете «недостатки» языка хоста

Java имеет «недостатки» в том, что каждый тип гарантированно наследуется от java.lang.Object и вместе с ним его методов: getClass() , clone() , finalize() equals() , hashCode() , toString() , notify() , notifyAll() и wait() .

В большинстве API это не такая уж большая проблема. Вам на самом деле не нужно повторно использовать любое из названных выше методов (пожалуйста, не используйте) .

Но при разработке внутреннего DSL эти имена методов Object (как и ключевые слова языка) ограничивают вас в пространстве разработки. Это особенно очевидно в случае equal(s) .

Мы узнали, и мы устарели и удалим having(Boolean) перегрузку и все подобные перегрузки снова.