X

Changer SELECT .. FROM en FROM .. SELECT ne “répare” pas SQL – Java, SQL et jOOQ.


De temps en temps, je vois des gens déplorer la déconnexion particulière de la syntaxe SQL entre

Plus récemment, ici, dans un commentaire Youtube, répondez à une récente conversation jOOQ / kotlin. Regardons pourquoi jOOQ n’est pas tombé dans ce piège d’essayer de “réparer” cela, et pourquoi c’est même un piège.

La langue anglaise

SQL a un modèle de syntaxe simple. Toutes les commandes commencent par un verbe à l’impératif, comme nous “commande” la base de données pour exécuter une instruction. Les commandes courantes incluent :

  • SELECT
  • INSERT
  • UPDATE
  • DELETE
  • MERGE
  • TRUNCATE
  • CREATE
  • ALTER
  • DROP

Ce sont tous des verbes à l’impératif. Pensez à ajouter un point d’exclamation partout, par exemple INSERT [this record]!

L’ordre des opérations

Nous pouvons affirmer que les langages naturels sont une très mauvaise source d’inspiration pour les langages de programmation informatique, qui ont tendance à être plus mathématiques (certains plus que d’autres). Beaucoup de critiques sur le langage SQL est qu’il ne “compose” pas (dans sa forme native).

Nous pouvons affirmer qu’il serait bien préférable pour un langage SQL plus composable de commencer par FROMqui est la première opération de SELECT selon l’ordre logique des opérations. Par exemple

FROM book
WHERE book.title LIKE 'A%'
SELECT book.id, book.title

Oui, ce serait mieux dans le sens où ce serait plus logique. Tout d’abord, nous déclarons la source de données, les prédicats, etc. et ce n’est qu’à la fin que nous déclarons la projection. Avec Java Stream API, nous écrirons :

books.stream()
     .filter(book -> book.title.startsWith("A"))
     .map(book -> new B(book.id, book.title))

Les avantages de ceci seraient :

  • Pas de déconnexion entre la syntaxe et la logique
  • Par conséquent : aucune confusion autour de la syntaxe, en particulier pourquoi vous ne pouvez pas référencer SELECT alias dans WHEREPar exemple.
  • Meilleure auto-complétion (parce que vous n’écrivez pas de choses qui ne sont pas déclarées encored’abord)

D’une certaine manière, cet ordre serait cohérent avec ce que certains SGBDR implémentaient lorsqu’ils RETURNING les données des instructions DML, telles que :

INSERT INTO book (id, title)
VALUES (3, 'The Book')
RETURNING id, created_at

Avec les instructions DML, la commande (“impératif”) est toujours INSERT, UPDATE, DELETEc’est-à-dire un verbe qui indique clairement à la base de données quoi faire avec les données. La « projection » est plus une réflexion après coup. Un utilitaire parfois utile, d’où RETURNING peut être placé à la fin.

RETURNING semble être un choix pragmatique de syntaxe et ne fait même pas partie de la norme. La norme définit la <data change delta table>tel qu’implémenté par Db2 et H2, dont la syntaxe est :

SELECT id, created_at
FROM FINAL TABLE (
  INSERT INTO book (id, title)
  VALUES (3, 'The Book')
) AS book

Je veux dire, pourquoi pas. Je n’ai pas de forte préférence pour l’une ou l’autre syntaxe (jOOQ prend en charge les deux et les émule l’une dans l’autre). SQL Server a inventé une troisième variante, dont la syntaxe est probablement la moins intuitive (je dois toujours chercher l’emplacement exact du OUTPUT clause):

INSERT INTO book (id, title)
OUTPUT id, created_at
VALUES (3, 'The Book')

Langage de requête chiffré

Il convient probablement de mentionner ici qu’il existe un langage de requête moderne qui est suffisamment populaire pour être pris en compte pour de telles discussions : le langage de requête Cypher de neo4j. Avec un simple “truc”, c’est à la fois :

  • Maintien du modèle de langage où un verbe à la forme impérative commence une déclaration (le verbe est MATCHqui est semblable à FROMmais c’est un verbe), il hérite donc de la “force” de SQL d’être intuitif également pour les non-programmeurs.
  • Inversé l’ordre logique des opérations dans les instructions de lecture, pour être de la forme MATCH .. RETURNfabrication RETURN la forme universelle de projection des choses pour toutes les opérations, pas seulement SELECT.
  • Réutilisé MATCH également pour les opérations d’écriture, y compris DELETE ou SET (ce qui correspond à SQL UPDATE)

Tout en opérant sur un paradigme de données différent (le modèle de réseau par opposition au modèle relationnel), j’ai toujours trouvé que le langage de requête Cypher était généralement supérieur à SQL en termes de syntaxe, du moins à un niveau élevé. Si je devais réellement “réparer” SQL en créant SQL 2.0, je m’en inspirerais ici.

Corriger cela dans une API comme jOOQ n’en vaut pas la peine

Comme indiqué précédemment, SQL présente des lacunes évidentes et il existe de meilleurs langages comme Cypher résolvant le même type de problème. Mais SQL est là, et il a 50 ans, et il restera. Ce ne sera pas réglé.

C’est quelque chose qu’il faut accepter :

SQL ne sera pas corrigé

Il sera amendé. Il intègre de nouvelles idées, notamment :

Il le fait toujours de manière idiomatique, dans le style SQL. Si vous lisez le standard SQL, ou si vous travaillez avec PostgreSQL, qui est très proche du standard, vous aurez l’impression que SQL est assez cohérent en tant que langage. Ou, c’est toujours bizarre, selon vos goûts.

Pour jOOQ, l’un des principaux facteurs de succès a toujours été d’être au plus près de cette vision de ce qu’est réellement SQL en termes de syntaxe. Beaucoup de gens sont très efficaces pour écrire du SQL natif. Étant donné que Java a des blocs de texte, il est devenu beaucoup plus supportable de simplement copier-coller une requête SQL statique de votre éditeur SQL dans votre programme Java, et par exemple de l’exécuter avec JDBC ou avec l’API de modélisation SQL simple de jOOQ :

for (Record record : ctx.fetch(
    """
    SELECT id, title
    FROM book
    WHERE title LIKE 'A%'
    """
)) {
    System.out.println(record);
}

Cette approche est suffisante pour très applications simples là-bas. Si votre “application” exécute un total de 5 requêtes SQL distinctes, vous pouvez le faire avec JDBC seul (bien qu’une fois que vous aurez commencé à vous familiariser avec jOOQ, vous utiliserez probablement jOOQ même pour ces applications également).

Mais jOOQ brille vraiment lorsque votre application contient des centaines de requêtes, y compris de nombreuses requêtes dynamiques, et que votre base de données contient des centaines de tables, dans le cas où les avantages de la sécurité du type et de la sécurité du modèle sont vraiment utiles. Cependant, il ne peut briller que lorsque votre requête SQL traduit 1: 1 vers l’API jOOQ. Correction aléatoire de SQL dans une certaine mesure dans cette déclaration la plus importante (SELECT) ne fera pas l’affaire.

Parce que : Où arrêterez-vous de réparer SQL ? SQL est toujours bizarre même si vous passez à FROM .. SELECT. Par exemple, la sémantique de GROUP BY est toujours bizarre. Ou la relation entre DISTINCT et ORDER BY. Par exemple, cela semble être beaucoup mieux au début (par exemple pour séparer SELECT et DISTINCTqui ne devraient pas être situés si près l’un de l’autre) :

FROM book
WHERE book.title LIKE 'A%'
SELECT book.title
DISTINCT
ORDER BY book.title

Mais les mises en garde étranges ne disparaîtraient toujours pas, à savoir que vous pouvez ORDER BY expressions qui ne sont pas répertoriées dans SELECT en l’absence de DISTINCTmais pas en présence de DISTINCT (voir notre article précédent à ce sujet).

Syntaxes alternatives dans d’autres API DSL

Alors, où s’arrête la « réparation » de SQL ? Quand SQL sera-t-il “réparé ?” Il ne sera jamais corrigé, et en tant que tel, une API comme jOOQ serait beaucoup plus difficile à apprendre qu’elle devrait l’être. Certaines API concurrentes suivent ce modèle, par exemple

Ces deux API sont basées sur l’idée que SQL a besoin d’être “réparé” et qu’une sensation plus “native”, plus “idiomatique” de l’API serait un peu meilleure. Quelques exemples:

Nappe:

Voici un exemple tiré du guide de démarrage :

Cela correspond au SQL suivant :

SELECT max(price)
FROM coffees

C’est sans doute un peu plus idiomatique. Cela ressemble à l’utilisation ordinaire de l’API de collection Scala, supprimant la sensation SQL de l’équation. Après tout, l’habituel map(x => y) les méthodes de collecte correspondent bien à un SQL SELECT clause (une “projection”).

Exposé:

Voici un exemple de Baeldung :

StarWarsFilms
  .slice(StarWarsFilms.sequelId.count(), StarWarsFilms.director)
  .selectAll()
  .groupBy(StarWarsFilms.director)

L’API introduit de nouveaux termes, par exemple

  • slice ce qui signifie la même chose que map() ou SELECTbien qu’étranger aux API de collecte SQL ou kotlin
  • selectAllqui correspond au terme d’algèbre relationnelle “sélection”, correspondant à SQL WHERE

Syntaxe de commodité synthétique au lieu de “réparer” SQL

jOOQ ne suit pas cette voie et ne le fera jamais. SQL est ce qu’il est, et jOOQ ne pourra pas “réparer” cela. Le mappage 1:1 entre la syntaxe SQL et l’API jOOQ signifie que même si vous souhaitez utiliser quelque chose de sophistiqué, comme :

Même alors, jOOQ ne vous laissera pas tomber et vous permettra d’écrire exactement ce que vous avez en tête en termes de fonctionnalité SQL. Je veux dire, serait-il vraiment judicieux de soutenir CONNECT BY en Slick ou Exposed ? Probablement pas. Ils devraient inventer leur propre syntaxe pour donner accès à la récursivité SQL. Mais sera-t-il complet ? C’est un problème que jOOQ n’aura pas.

La seule raison pour laquelle une syntaxe n’est pas disponible est que ce n’est pas possible encore (et veuillez envoyer une demande de fonctionnalité). L’exemple de FOR XML est un excellent. SQL Server a inventé ceci FOR clause, et bien qu’elle soit pratique pour les cas simples, elle n’est pas très puissante pour les cas complexes. Je préfère de loin la syntaxe standard SQL/XML et SQL/JSON (que jOOQ aussi les soutiens). Mais en même temps je n’aime pas beaucoup la syntaxe, jOOQ ne jugera pas. A quoi servirait une troisième syntaxe, entièrement inventée par jOOQ pour les utilisateurs ? Comme je le disais avant.

Quand le “fixing” s’arrêtera-t-il ?

Cela ne s’arrêtera jamais. Les alternatives que j’ai mentionnées se heurteront à des questions très difficiles lorsqu’elles commenceront à ajouter plus de fonctionnalités, si elles commencent à ajouter plus de fonctionnalités. S’il est toujours facile de mettre en œuvre un simple SELECT .. FROM .. WHERE générateur de requêtes et prend en charge cette fonctionnalité à l’aide d’une API arbitraire, affirmant que SQL a été “corrigé”, il est beaucoup plus difficile de faire évoluer cette API, en traitant toutes sortes de cas d’utilisation SQL avancés. Il suffit de regarder leurs outils de suivi des problèmes pour les demandes de fonctionnalités telles que les CTE. La réponse est toujours : “Utilisez le SQL natif”.

Même les fonctionnalités SQL “simples”, telles que UNION deviennent plus complexes une fois que la syntaxe SQL de base est modifiée. La sémantique est déjà assez compliquée en SQL (et c’est entièrement la faute de SQL, bien sûr), mais “réparer” ces choses n’est jamais aussi simple qu’il n’y paraît au premier abord.

Maintenant, il y a 2 exceptions à cette règle :

Syntaxe synthétique

Une exception est : la “syntaxe synthétique”. La syntaxe synthétique la plus puissante dans jOOQ sont les jointures implicites. Les jointures implicites ne « corrigent » pas SQL, elles « améliorent » SQL avec une syntaxe que SQL lui-même pourrait avoir (espérons-le, éventuellement). Tout comme il existe des dialectes SQL, qui “améliorent” le standard SQL, par exemple

jOOQ est très conservateur sur une telle syntaxe synthétique. Il y a beaucoup de bonnes idées, mais peu sont compatibles. Chacune de ces syntaxes rend les autres fonctionnalités de transformation SQL plus complexes, et chacune présente des défauts qui n’ont peut-être pas encore été résolus (par exemple, à partir de jOOQ 3.16, les jointures implicites ne sont pas possibles dans les instructions DML telles que UPDATE, DELETE, même s’ils ont aussi beaucoup de sens là-bas. Voir numéro 7508).

Syntaxe de commodité

Un autre type d’amélioration est ce que j’appelle la “syntaxe de commodité”. Par exemple, quel que soit le SGBDR sous-jacent, jOOQ vous permet d’écrire :

select(someFunction()); // No FROM clause
selectFrom(someTable);  // No explicit SELECT list

Dans les deux cas, les utilisateurs peuvent omettre clauses qui peuvent être obligatoires dans le dialecte SQL sous-jacent, et jOOQ remplit le SQL généré avec une valeur par défaut raisonnable :

  • UN FROM DUAL déclaration de table, ou quelque chose de similaire
  • UN SELECT * déclaration de projection, ou quelque chose de similaire

Conclusion

L’idée que jOOQ devrait s’en tenir à la syntaxe SQL sur une base 1: 1 était un pari que j’ai pris il y a 13 ans, lorsque j’ai créé jOOQ. Je voulais concevoir jOOQ de manière à ce que tous ceux qui connaissaient déjà SQL n’aient aucun problème à apprendre jOOQ, car tout est absolument simple. La technique derrière cette conception d’API est décrite ici.

D’autres ont tenté de “réparer” SQL soit en rendant leur API très idiomatique compte tenu du langage cible, soit en inventant un nouveau langage.

13 ans plus tard, j’ai découvert que l’approche d’imitation 1: 1 est la seule viable, car je continue à découvrir de nouvelles fonctionnalités SQL obscures :

Créer un langage est incroyablement difficile (considérons une API DSL interne comme une sorte de langage). Il est presque impossible de concevoir correctement, si l’objectif est de prendre en charge à peu près n’importe quelle fonctionnalité SQL sous-jacente, sauf si, le designer abandonne ce rêve de «réparer» les choses et commence à embrasser le «rêve» de «soutenir» les choses. Toutes les choses.

SQL est ce qu’il est. Et cela signifie que la syntaxe est SELECT .. FROMpas FROM .. SELECT.