I. Prérequis▲
Vous pouvez utiliser l'IDE que vous souhaitez pour suivre ce tutoriel. Le code source des exemples présentés dans ce tutoriel est disponible dans ce repository Git, et plus particulièrement dans cette classe.
Les outils dont vous aurez besoin pour suivre ce tutoriel sont : JDK 8 d'Oracle, Maven 3.
N'hésitez pas à lire le tutoriel Maven 3 si vous souhaitez en apprendre plus sur ce dernier.
Avoir un SGBDR installé sur votre machine est un plus pour tester les exemples SQL. Si vous souhaitez tester les requêtes SQL qui vont suivre, vous pouvez exécuter le script ci-dessous dans une base de test :
CREATE
TABLE
employees (
gender VARCHAR
(
100
)
,
age INTEGER
,
salary INTEGER
,
name
VARCHAR
(
100
)
)
;
INSERT
INTO
employees (
gender, age, salary, name
)
VALUES
(
'M'
, 40
, 9800
, 'Bruce WAYNE'
)
,
(
'M'
, 33
, 1500
, 'Clark KENT'
)
,
(
'M'
, 23
, 4500
, 'Barry ALLEN'
)
,
(
'M'
, 19
, 2400
, 'Wally WEST'
)
,
(
'M'
, 28
, 8000
, 'Hal JORDAN'
)
,
(
'M'
, 35
, 9500
, 'Oliver QUEEN'
)
,
(
'M'
, 42
, 4700
, 'Ray PALMER'
)
,
(
'M'
, 22
, 3600
, 'Victor Stone'
)
,
(
'M'
, 27
, 1500
, 'John CONSTANTINE'
)
,
(
'M'
, 65
, 2600
, 'J''onn J''ONZZ'
)
,
(
'M'
, 28
, 1400
, 'Arthur CURRY'
)
,
(
'M'
, 25
, 2500
, 'Dick GRAYSON'
)
,
(
'F'
, 65
, 6000
, 'Diana PRINCE'
)
,
(
'F'
, 24
, 2500
, 'Barbara GORDON'
)
,
(
'F'
, 36
, 1800
, 'Selina KYLE'
)
,
(
'F'
, 30
, 2400
, 'Pamela ISLEY'
)
,
(
'F'
, 28
, 2400
, 'Harleen QUINZEL'
)
,
(
'F'
, 29
, 2000
, 'Zatanna ZATARA'
)
;
Voici un aperçu des données Java :
public
static
List<
Employee>
employees
(
) {
return
new
ArrayList<
Employee>(
){{
add
(
Employee.builder
(
).gender
(
"M"
).age
(
40
).salary
(
9800
).name
(
"Bruce WAYNE"
).build
(
));
add
(
Employee.builder
(
).gender
(
"M"
).age
(
33
).salary
(
1500
).name
(
"Clark KENT"
).build
(
));
add
(
Employee.builder
(
).gender
(
"M"
).age
(
23
).salary
(
4500
).name
(
"Barry ALLEN"
).build
(
));
add
(
Employee.builder
(
).gender
(
"M"
).age
(
19
).salary
(
2400
).name
(
"Wally WEST"
).build
(
));
add
(
Employee.builder
(
).gender
(
"M"
).age
(
28
).salary
(
8000
).name
(
"Hal JORDAN"
).build
(
));
add
(
Employee.builder
(
).gender
(
"M"
).age
(
35
).salary
(
9500
).name
(
"Oliver QUEEN"
).build
(
));
add
(
Employee.builder
(
).gender
(
"M"
).age
(
42
).salary
(
4700
).name
(
"Ray PALMER"
).build
(
));
add
(
Employee.builder
(
).gender
(
"M"
).age
(
22
).salary
(
3600
).name
(
"Victor Stone"
).build
(
));
add
(
Employee.builder
(
).gender
(
"M"
).age
(
27
).salary
(
1500
).name
(
"John CONSTANTINE"
).build
(
));
add
(
Employee.builder
(
).gender
(
"M"
).age
(
65
).salary
(
2600
).name
(
"J'onn J'ONZZ"
).build
(
));
add
(
Employee.builder
(
).gender
(
"M"
).age
(
28
).salary
(
1400
).name
(
"Arthur CURRY"
).build
(
));
add
(
Employee.builder
(
).gender
(
"M"
).age
(
25
).salary
(
2500
).name
(
"Dick GRAYSON"
).build
(
));
add
(
Employee.builder
(
).gender
(
"F"
).age
(
65
).salary
(
6000
).name
(
"Diana PRINCE"
).build
(
));
add
(
Employee.builder
(
).gender
(
"F"
).age
(
24
).salary
(
2500
).name
(
"Barbara GORDON"
).build
(
));
add
(
Employee.builder
(
).gender
(
"F"
).age
(
36
).salary
(
1800
).name
(
"Selina KYLE"
).build
(
));
add
(
Employee.builder
(
).gender
(
"F"
).age
(
30
).salary
(
2400
).name
(
"Pamela ISLEY"
).build
(
));
add
(
Employee.builder
(
).gender
(
"F"
).age
(
28
).salary
(
2400
).name
(
"Harleen QUINZEL"
).build
(
));
add
(
Employee.builder
(
).gender
(
"F"
).age
(
29
).salary
(
2000
).name
(
"Zatanna ZATARA"
).build
(
));
}}
;
}
II. Les bases▲
L'API Stream est inspirée des langages fonctionnels, elle permet de réaliser diverses opérations sur des ensembles en utilisant le fluent pattern (ou « chaînage de méthodes » en français). Pour notre cas, les collections (ou « listes » si vous préférez) représenteront ces ensembles.
L'API Stream ne permet pas de réaliser des choses qu'il n'était pas possible de faire avant, par contre elle améliore grandement la lisibilité du code (à moins d'en abuser bien évidemment). Voici deux algorithmes qui permettent de récupérer les trois employés (homme) les plus riches de l'entreprise :
Sans l'API Stream :
List<
Employee>
temp =
new
ArrayList<>(
);
for
(
Employee employee : employees
(
)) {
if
(
"M"
.equals
(
employee.getGender
(
))) {
// WHERE gender = 'M'
temp.add
(
employee);
}
}
Collections.sort
(
temp, (
e1, e2) ->
e2.getSalary
(
) -
e1.getSalary
(
)); // ORDER BY salary DESC
int
limit =
(
temp.size
(
) >
0
&&
temp.size
(
) <=
3
) ? temp.size
(
) : 3
;
List<
Employee>
result =
(
temp.isEmpty
(
)) ? Collections.emptyList
(
) : temp.subList
(
0
, limit); // LIMIT X
// Print
System.out.println
(
"Salary | Name"
);
result.forEach
(
employee ->
System.out.println
(
String.format
(
"%s | %s"
, employee.getSalary
(
), employee.getName
(
))
)
);
Avec l'API Stream :
List<
Employee>
result =
employees
(
).stream
(
)
.filter
(
employee ->
"M"
.equals
(
employee.getGender
(
))) // WHERE gender = 'M'
.sorted
((
e1, e2) ->
e2.getSalary
(
) -
e1.getSalary
(
)) // ORDER BY salary DESC
.limit
(
3
) // LIMIT X
.collect
(
Collectors.toList
(
));
// Print
System.out.println
(
"Salary | Name"
);
result.forEach
(
employee ->
System.out.println
(
String.format
(
"%s | %s"
, employee.getSalary
(
), employee.getName
(
))
)
);
Pour les exemples qui vont suivre, nous allons d'un côté réaliser des opérations sur une table en SQL, et de l'autre sur une liste d'objets en Java. Reprenons l'exemple précédent :
en SQL :
SELECT
*
FROM
employees
WHERE
gender =
'M'
ORDER
BY
salary DESC
LIMIT
3
;
avec l'API Stream en Java :
List<
Employee>
result =
employees
(
).stream
(
)
.filter
(
employee ->
"M"
.equals
(
employee.getGender
(
))) // WHERE gender = 'M'
.sorted
((
e1, e2) ->
e2.getSalary
(
) -
e1.getSalary
(
)) // ORDER BY salary DESC
.limit
(
3
) // LIMIT X
.collect
(
Collectors.toList
(
));
Grâce à cet exemple de base, nous avons vu comment filtrer, ordonner et limiter le résultat.
L'exemple ci-dessous permet d'ajouter plusieurs critères pour ordonner le résultat :
en SQL :
SELECT
*
FROM
employees
ORDER
BY
gender ASC
,
salary ASC
,
name
ASC
;
avec l'API Stream en Java :
List<
Employee>
result =
employees
(
).stream
(
)
.sorted
(
Comparator.comparing
(
Employee::getGender) // ORDER BY gender ASC
.thenComparing
(
Employee::getSalary) // , salary ASC
.thenComparing
(
Employee::getName) // , name ASC
)
.collect
(
Collectors.toList
(
));
III. Les fonctions d'agrégat▲
Les fonctions d'agrégat permettent, comme leur nom indique, d'agréger un ensemble. Voici quelques opérations d'agrégat sur notre liste d'employés.
III-A. MIN : Plus jeune âge▲
En SQL :
SELECT
MIN
(
age)
AS
youngestAge
FROM
employees;
Avec l'API Stream en Java :
Optional<
Integer>
youngestAge =
employees
(
).stream
(
)
.map
(
Employee::getAge)
.min
(
Integer::compare); // MIN(age)
III-B. MAX : Plus haut salaire▲
En SQL :
SELECT
MAX
(
salary)
AS
highestSalary
FROM
employees;
Avec l'API Stream en Java :
Optional<
Integer>
highestSalary =
employees
(
).stream
(
)
.map
(
Employee::getSalary)
.max
(
Integer::compare); // MAX(salary)
III-C. AVG : Moyenne d'âge des employés▲
En SQL :
SELECT
AVG
(
age)
AS
averageAge
FROM
employees;
Avec l'API Stream en Java :
OptionalDouble averageAge =
employees
(
).stream
(
)
.mapToInt
(
Employee::getAge)
.average
(
); // AVG(age)
III-D. COUNT : Nombre de femmes▲
En SQL :
SELECT
COUNT
(*)
AS
womenCount
FROM
employees WHERE
gender =
'F'
;
Avec l'API Stream en Java :
long
womenCount =
employees
(
).stream
(
)
.filter
(
employee ->
"F"
.equals
(
employee.getGender
(
))) // WHERE gender = 'F'
.count
(
); // COUNT(*)
III-E. SUM : Somme des salaires avec 21,7 % de taxe▲
En SQL :
SELECT
SUM
(
salary)
*
1
.217
AS
salarySumWithTaxes
FROM
employees;
Avec l'API Stream en Java :
double
salarySumWithTaxes =
employees
(
).stream
(
)
.mapToDouble
(
Employee::getSalary)
.sum
(
) *
1.217
; // SUM(salary) * 1.217
IV. La syntaxe Reduce▲
Il est possible de réaliser l'ensemble des opérations effectuées dans la partie III manuellement grâce à la méthode de base reduce
(
).
La méthode reduce
(
) possède plusieurs signatures de méthode. Nous en étudierons deux :
- Optional<T> reduce(BinaryOperator<T> accumulator) ;
- T reduce(T valeurInitiale, BinaryOperator<T> accumulator).
La méthode reduce
(
) prend en paramètre un lambda avec deux paramètres, le premier paramètre du lambda représente le résultat des calculs effectués précédemment et le second paramètre le nouvel élément du stream, l'implémentation de votre lambda contiendra donc l'opération que vous comptez effectuer entre le résultat de vos précédentes opérations et le nouvel élément du stream.
À savoir
- La première méthode retourne un optional. Si vous utilisez la première méthode et que votre stream ne contient aucun élément, vous obtiendrez un optional vide.
- La deuxième méthode retourne directement votre résultat. Le premier paramètre de la deuxième méthode représente la valeur initiale de votre stream, et ce sera la valeur retournée si votre stream ne contient aucun élément.
- Les deux méthodes ont un intérêt différent, par exemple, si vous souhaitez relever le plus haut salaire mensuel à partir de 10 000 €, mais que votre stream ne contient aucun salaire aussi élevé, il serait peut-être préférable d'utiliser la première méthode et de retourner un optional vide plutôt que 0. Par contre, si vous souhaitez retourner le nombre de femmes et qu'il n'y a pas de femme dans l'entreprise, il serait peut-être préférable d'utiliser la deuxième et de retourner 0 plutôt qu'un optional vide.
- Si votre stream ne contient qu'un seul élément, le résultat de votre reduce sera cet élément.
- Si vous utilisez la première méthode, lors de la première opération, le premier paramètre de votre lambda vaut le premier élément du stream et le second paramètre vaut le second élément du stream. Si vous utilisez la deuxième méthode, lors de la première opération, le premier paramètre de votre lambda vaut l'opération réalisée entre la valeur initiale de votre stream et le premier élément du stream. Lors des autres passages, le premier paramètre de votre lambda vaudra le résultat de vos précédentes opérations, et le second paramètre le nouvel élément du stream.
IV-A. MIN : Plus jeune âge▲
Optional<
Integer>
youngestAge =
employees
(
).stream
(
)
.map
(
Employee::getAge)
.reduce
((
a, b) ->
(
a <
b) ? a : b); // MIN(age)
IV-B. MAX : Plus haut salaire▲
Optional<
Integer>
highestSalary =
employees
(
).stream
(
)
.map
(
Employee::getSalary)
.reduce
((
a, b) ->
(
a >
b) ? a : b); // MAX(age)
IV-C. AVG : Moyenne d'âge des employés▲
// AVG => SUM() / COUNT(*)
AtomicInteger count =
new
AtomicInteger
(
1
);
OptionalDouble sumAge =
employees
(
).stream
(
)
.mapToDouble
(
Employee::getAge)
.reduce
((
a, b) ->
{
count.incrementAndGet
(
);
return
a +
b;
}
);
OptionalDouble averageAge =
sumAge.isPresent
(
) ?
OptionalDouble.of
(
sumAge.getAsDouble
(
) /
count.get
(
)) : OptionalDouble.empty
(
);
IV-D. COUNT : Nombre de femmes▲
long
womenCount =
employees
(
).stream
(
)
.filter
(
employee ->
"F"
.equals
(
employee.getGender
(
))) // WHERE gender = 'F'
.map
(
e ->
1
L)
.reduce
(
0
L, (
a, b) ->
a +
b); // COUNT(*)
IV-E. SUM : Somme des salaires avec 21,7 % de taxe▲
double
salarySumWithTaxes =
employees
(
).stream
(
)
.map
(
Employee::getSalary)
.reduce
(
0
, (
a, b) ->
(
a +
b)) *
1.217
; // SUM(salary) * 1.217
V. Le regroupement▲
Les streams permettent de réaliser des regroupements.
V-A. La syntaxe GROUP BY▲
En SQL :
SELECT
salary
FROM
employees
GROUP
BY
salary;
Avec l'API Stream en Java :
Map<
Integer, List<
Employee>>
genders2 =
employees
(
).stream
(
)
.collect
(
Collectors.groupingBy
(
Employee::getSalary)); // GROUP BY
// Print
System.out.println
(
String.format
(
"Genders: %s"
, genders2.entrySet
(
).stream
(
)
.map
(
Map.Entry::getKey)
.map
(
Object::toString)
.collect
(
Collectors.joining
(
", "
))
));
V-B. La syntaxe DISTINCT▲
En SQL :
SELECT
DISTINCT
salary
FROM
employees;
Avec l'API Stream en Java :
List<
Integer>
genders1 =
employees
(
).stream
(
)
.map
(
Employee::getSalary)
.distinct
(
) // DISTINCT
.collect
(
Collectors.toList
(
));
// Print
System.out.println
(
String.format
(
"Genders: %s"
,
genders1.stream
(
)
.map
(
Object::toString)
.collect
(
Collectors.joining
(
", "
))
));
V-C. Exemple avec MAX▲
En SQL :
SELECT
gender, MAX
(
salary)
AS
salary
FROM
employees
GROUP
BY
gender;
Avec l'API Stream en Java :
Map<
String, Optional<
Integer>>
result =
employees
(
).stream
(
)
.collect
(
Collectors.groupingBy
(
Employee
:
:getGender, // GROUP BY gender
Collectors.mapping
(
Employee::getSalary, Collectors.maxBy
(
Integer::compare)) // MAX(salary)
));
// Print
result.entrySet
(
).forEach
(
entry ->
System.out.println
(
String.format
(
"Gender: %s | Max Salary: %s"
, entry.getKey
(
), entry.getValue
(
)))
);
V-D. Exemple avec AVG▲
En SQL :
SELECT
gender, AVG
(
salary)
AS
salary
FROM
employees
GROUP
BY
gender;
Avec l'API Stream en Java :
Map<
String, Double>
result =
employees
(
).stream
(
)
.collect
(
Collectors.groupingBy
(
Employee
:
:getGender, // GROUP BY gender
Collectors.averagingDouble
(
Employee::getSalary) // AVG(salary)
));
// Print
result.entrySet
(
).forEach
(
entry ->
System.out.println
(
String.format
(
"Gender: %s | Average Salary: %s"
, entry.getKey
(
), entry.getValue
(
)))
);
V-E. Exemple avec COUNT▲
En SQL :
SELECT
gender, COUNT
(*)
AS
count
FROM
employees
GROUP
BY
gender;
Avec l'API Stream en Java :
Map<
String, Long>
result =
employees
(
).stream
(
)
.collect
(
Collectors.groupingBy
(
Employee
:
:getGender, // GROUP BY gender
Collectors.counting
(
) // COUNT(*)
));
// Print
result.entrySet
(
).forEach
(
entry ->
System.out.println
(
String.format
(
"Gender: %s | Count: %s"
, entry.getKey
(
), entry.getValue
(
)))
);
VI. La jointure cartésienne▲
Les streams permettent de réaliser des jointures cartésiennes.
En SQL :
SELECT
t1.e AS
x
, t2.e AS
y
, t3.e AS
z
FROM
t1
CROSS
JOIN
t2
CROSS
JOIN
t3;
Avec l'API Stream en Java :
String[] t1 =
{
"A"
, "B"
, "C"
}
;
String[] t2 =
{
"B"
, "C"
, "D"
}
;
String[] t3 =
{
"C"
, "F"
, "E"
}
;
// CROSS JOIN
List<
Container>
list =
Stream.of
(
t1)
.flatMap
(
x ->
Arrays.stream
(
t2) // t1 CROSS JOIN t2
.flatMap
(
y ->
Arrays.stream
(
t3) // t2 CROSS JOIN t3
.map
(
z ->
new
Container
(
x, y, z))
)
)
.collect
(
Collectors.toList
(
));
// Print
list.forEach
(
System.out::println);
VII. La jointure interne▲
Les streams permettent de réaliser des jointures internes.
En SQL :
SELECT
t1.e AS
x
, t2.e AS
y
, t3.e AS
z
FROM
t1
INNER
JOIN
t2
ON
t1.e =
t2.e
INNER
JOIN
t3
ON
t2.e =
t3.e;
Avec l'API Stream en Java :
String[] t1 =
{
"A"
, "B"
, "C"
}
;
String[] t2 =
{
"B"
, "C"
, "D"
}
;
String[] t3 =
{
"C"
, "F"
, "E"
}
;
// INNER JOIN
List<
Container>
list =
Stream.of
(
t1)
.flatMap
(
x ->
Arrays.stream
(
t2) // t1 INNER JOIN t2
.filter
(
y ->
Objects.equals
(
x, y)) // ON t1.e = t2.e
.flatMap
(
y ->
Arrays.stream
(
t3) // t2 INNER JOIN t3
.filter
(
z ->
Objects.equals
(
y, z)) // ON t2.e = t3.e
.map
(
z ->
new
Container
(
x, y, z))
)
).collect
(
Collectors.toList
(
));
// Print
list.forEach
(
System.out::println);
VIII. Les limites de l'API Stream▲
L'API stream permet de réaliser avec une grande aisance des opérations sur une collection, mais son utilisation possède aussi des limites lorsqu'il s'agit d'effectuer des jointures entre plusieurs collections. En effet, la syntaxe peut porter à confusion au fur et à mesure qu'une liste est ajoutée dans la chaîne d'opérations, de plus, les streams ne sont pas adaptés pour réaliser des jointures externes (cf. : LEFT
JOIN
/ RIGHT
JOIN
/ FULL
JOIN
).
Quoi de mieux que du SQL pour faire du SQL ? Et si nous essayions de manipuler des collections d'objets Java directement en SQL ? C'est ce que permet par exemple de faire l'API InMemorySQL du projet dbms-replication. Voici la dépendance Maven pour l'importer dans votre projet :
<dependency>
<groupId>
com.github.gokan-ekinci</groupId>
<artifactId>
dbms-replication</artifactId>
<version>
1.0</version>
</dependency>
Si nous reprenons l'exemple de la jointure cartésienne, cela donne :
List<
Tuple>
t1 =
tupleInit
(
"A"
, "B"
, "C"
);
List<
Tuple>
t2 =
tupleInit
(
"B"
, "C"
, "D"
);
List<
Tuple>
t3 =
tupleInit
(
"C"
, "D"
, "E"
);
String request =
"SELECT t1.e AS x, t2.e AS y, t3.e AS z FROM t1 CROSS JOIN t2 CROSS JOIN t3"
;
new
InMemorySQL
(
)
.add
(
Tuple.class
, t1)
.add
(
Tuple.class
, t2)
.add
(
Tuple.class
, t3)
.executeQuery
(
Container.class
, request)
.forEach
(
System.out::println);
Pour la jointure interne, cela donne :
List<
Tuple>
t1 =
tupleInit
(
"A"
, "B"
, "C"
);
List<
Tuple>
t2 =
tupleInit
(
"B"
, "C"
, "D"
);
List<
Tuple>
t3 =
tupleInit
(
"C"
, "D"
, "E"
);
String request =
"SELECT t1.e AS x, t2.e AS y, t3.e AS z "
+
"FROM t1 "
+
"INNER JOIN t2 "
+
"ON t1.e = t2.e "
+
"INNER JOIN t3 "
+
"ON t2.e = t3.e"
;
new
InMemorySQL
(
)
.add
(
Tuple.class
, t1)
.add
(
Tuple.class
, t2)
.add
(
Tuple.class
, t3)
.executeQuery
(
Container.class
, request)
.forEach
(
System.out::println);
IX. Remerciements▲
Je tiens à remercier Mickael Baron pour la relecture technique et ClaudeLELOUP pour la relecture orthographique.