Salta el contingut

6. Aprofundim en HQL

1. La base de dades

BBDD

Aquest document conté 30 exercicis de dificultat progressiva basats en l'esquema de base de dades escolar (Alumne, Professor, Mòdul, Docència, Examen).

2. Secció 1. SQL. Enunciats

2.1. Nivell 2: Agregació Simple (SELECT i WHERE)

  1. Obtenir totes les dades de tots els alumnes.
  2. Llistar només el nom i cognoms dels alumnes.
  3. Obtenir les dades dels alumnes que tinguin una edat major o igual a 20 anys.
  4. Llistar els alumnes que són repetidors (el camp repetidor és 1 o true).
  5. Llistar els alumnes ordenats per edat de forma descendent (del més gran al més jove).

2.2. Nivell 3: Unions Bàsiques (COUNT, AVG, MAX, MIN)

  1. Comptar quants alumnes hi ha registrats a la base de dades.
  2. Calcular l'edat mitjana dels alumnes.
  3. Obtenir la nota més alta registrada a la taula d'exàmens.
  4. Calcular la suma total de totes les edats dels alumnes.
  5. Comptar quants professors hi ha registrats.

2.3. Nivell 4: Unions Complexes (INNER JOIN)

  1. Llistar el nom de l'alumne i la nota de cada examen realitzat.
  2. Llistar el nom del mòdul i la nota de cada examen, ordenat per nota de major a menor.
  3. Mostrar el nom del professor i el nom dels mòduls que imparteix (Unió de Professor, Docència i Mòdul).
  4. Llistar els noms dels alumnes que s'han examinat del mòdul amb id 2 ('FOL'), juntament amb la seva nota.
  5. Mostrar el nom de l'alumne, nom del mòdul i la nota de tots els exàmens.

2.4. Nivell 5: Unions Externes Avançades (LEFT/RIGHT JOIN)

  1. Llistar tots els alumnes i les seves notes d'examen. Si un alumne no ha fet exàmens, ha d'aparèixer a la llista amb la nota com NULL.
  2. Trobar els alumnes que NO han realitzat cap examen (els que no tenen registres a la taula Examen).
  3. Llistar tots els mòduls i els professors que els imparteixen. Si un mòdul no té professor assignat (encara que en les nostres dades tots en tenen), hauria d'aparèixer.
  4. Mostrar els exàmens realitzats en mòduls que imparteix el professor 'Mariano Faus'.

2.5. Nivell 6: Agrupació Complexa (GROUP BY i HAVING)

  1. Calcular la nota mitjana de cada alumne (mostrar idAlumne i la mitjana).
  2. Calcular la nota mitjana de cada alumne, però mostrant el nom de l'alumne en lloc del seu ID.
  3. Mostrar quants exàmens s'han realitzat per cada mòdul (mostrar nom del mòdul i quantitat).
  4. Llistar els alumnes (nom) la nota mitjana dels quals sigui superior a 5.
  5. Mostrar el nom del mòdul i la nota mitjana dels exàmens d'aquest mòdul, només pels mòduls la mitjana dels quals sigui aprovada (major o igual a 5).
  6. Comptar quants mòduls imparteix cada professor (mostrar nom del professor i quantitat).

2.6. Nivell 7: Consultes Molt Complexes, Subconsultes Avançades i Lògica

  1. Obtenir el nom de l'alumne que va treure la nota més alta de tots els exàmens registrats (usant subconsulta o límit).
  2. Llistar els alumnes que tenen una edat superior a la mitjana d'edat de tots els alumnes.
  3. Obtenir els noms dels alumnes que han tret alguna nota per sota de 5 (suspesos).
  4. Mostrar el nom de l'alumne i una columna extra anomenada 'Estat' que digui 'APROVAT' si la seva nota a l'examen va ser >= 5 o 'SUSPES' si va ser < 5 (Ús de CASE).
  5. El Repte Final: Llistar el nom de cada professor i la quantitat total d'exàmens que han corregit (assumint que si un professor imparteix un mòdul, ell corregeix tots els exàmens d'aquest mòdul).

3. Secció 2. SQL. Solucions Explicades

A continuació, presento les consultes SQL per resoldre els exercicis. Recorda que en SQL a vegades hi ha vàries formes d'arribar al mateix resultat.

3.1. Nivell 2: Consultes Bàsiques

1. Obtenir totes les dades de tots els alumnes.

SQL
SELECT * FROM Alumne;

Explicació: L'asterisc * selecciona totes les columnes de la taula.

3.1.1. Llistar només el nom i cognoms dels alumnes.

SQL
SELECT nom, cognoms FROM Alumne;

Explicació: Especifiquem les columnes exactes per obtenir una projecció més neta.

3.1.2. Alumnes amb edat major o igual a 20.

SQL
SELECT * FROM Alumne WHERE edat >= 20;

Explicació: Filtrem files usant la clàusula WHERE i operadors de comparació.

3.1.3. Alumnes repetidors.

SQL
SELECT * FROM Alumne WHERE repetidor = 1;
-- O alternativament en alguns dialectes: WHERE repetidor = TRUE

Explicació: Essent un camp BIT (Booleana), el comparem amb 1 o TRUE

3.1.4. Alumnes ordenats per edat descendent.

SQL
SELECT * FROM Alumne ORDER BY edat DESC;

Explicació: ORDER BY defineix el criteri d'ordenació, i DESC inverteix l'ordre natural.

3.2. Nivell 3: Agregació Simple

3.2.1. Comptar alumnes.

SQL
SELECT COUNT(*) AS total_alumnes FROM Alumne;

Explicació: COUNT(*) compta el nombre total de files. AS ens permet donar-li un àlies a la columna resultant.

3.2.2. Edat mitjana.

SQL
SELECT AVG(edat) FROM Alumne;

Explicació: AVG calcula el promig numèric de la columna indicada.

3.2.3. Nota més alta.

SQL
SELECT MAX(nota) FROM Examen;

Explicació: MAX busca el valor màxim en la columna nota.

3.2.4. Suma d'edats.

SQL
SELECT SUM(edat) FROM Alumne;

Explicació: SUM realitza el sumatori dels valors.

3.2.5. Comptar professors.

SQL
SELECT COUNT(*) FROM Professor;

3.3. Nivell 4: Unions Bàsiques (JOINs)

3.3.1. Nom de l'alumne i nota de l'examen.

SQL
1
2
3
SELECT a.nom, e.nota
FROM Examen e
INNER JOIN Alumne a ON e.idAlumne = a.idAlumne;
Explicació: Unim Examen amb Alumne usant la clau forana. Usem àlies (e, a) per escriure menys.

3.3.2. Nom del mòdul i nota, ordenat.

SQL
1
2
3
4
SELECT m.nom, e.nota
FROM Examen e
INNER JOIN Modul m ON e.idModul = m.idModul
ORDER BY e.nota DESC;

3.3.3. Professor i mòduls que imparteix.

SQL
1
2
3
4
SELECT p.nom AS professor, m.nom AS modul
FROM Professor p
JOIN Docencia d ON p.idProfessor = d.idProfessor
JOIN Modul m ON d.idModul = m.idModul;
Explicació: Aquesta és una relació N:M. Necessitem dos JOIN per saltar de Professor a la taula intermèdia (Docencia) i de là a Modul.

3.3.4. Alumnes examinats del mòdul 2 (FOL).

SQL
1
2
3
4
SELECT a.nom, e.nota
FROM Alumne a
JOIN Examen e ON a.idAlumne = e.idAlumne
WHERE e.idModul = 2;
Explicació: Fem el JOIN i després filtrem per l'ID del mòdul a la taula d'exàmens.

3.3.5. Detall complet: Alumne, Mòdul i Nota.

SQL
1
2
3
4
SELECT a.nom AS Alumne, m.nom AS Modul, e.nota
FROM Examen e
JOIN Alumne a ON e.idAlumne = a.idAlumne
JOIN Modul m ON e.idModul = m.idModul;
Explicació: Unim les tres taules principals implicades en una qualificació.

3.4. Nivell 5: Unions Externes (LEFT JOIN)

3.4.1. Tots els alumnes i les seves notes (fins i tot si no en tenen).

SQL
1
2
3
SELECT a.nom, e.nota
FROM Alumne a
LEFT JOIN Examen e ON a.idAlumne = e.idAlumne;

Explicació: LEFT JOIN garanteix que es tragin totes les files de la taula de l'esquerra (Alumne), omplint amb NULL la part de la dreta (Examen) si no hi ha coincidència.

3.4.2. Alumnes que NO han fet exàmens.

SQL
1
2
3
4
SELECT a.nom
FROM Alumne a
LEFT JOIN Examen e ON a.idAlumne = e.idAlumne
WHERE e.idExamen IS NULL;

Explicació: Usem el LEFT JOIN anterior i filtrem on la clau primària de la taula dreta sigui NULL. Això aïlla els que no tenen relació.

3.4.3. Tots els mòduls i els seus professors.

SQL
1
2
3
4
SELECT m.nom, p.nom
FROM Modul m
LEFT JOIN Docencia d ON m.idModul = d.idModul
LEFT JOIN Professor p ON d.idProfessor = p.idProfessor;
Explicació: Assegurem llistar tots els mòduls, encara que no tinguessin professor assignat (encara que en les teves dades tots en tenen).

3.4.4. Exàmens de mòduls impartits per 'Mariano Faus'.

SQL
1
2
3
4
5
6
SELECT e.*
FROM Examen e
JOIN Modul m ON e.idModul = m.idModul
JOIN Docencia d ON m.idModul = d.idModul
JOIN Professor p ON d.idProfessor = p.idProfessor
WHERE p.nom = 'Mariano Faus';
Explicació: Connectem l'examen amb el mòdul, i el mòdul amb el professor per poder filtrar per el nom del docent.

3.5. Nivell 6: Agrupació (GROUP BY)

3.5.1. Nota mitjana per ID d'alumne.

SQL
1
2
3
SELECT idAlumne, AVG(nota) AS media
FROM Examen
GROUP BY idAlumne;
Explicació: Agrupem els resultats per alumne abans de calcular el promig.

3.5.2. Nota mitjana per Nom d'alumne.

SQL
1
2
3
4
SELECT a.nom, AVG(e.nota) AS media
FROM Alumne a
JOIN Examen e ON a.idAlumne = e.idAlumne
GROUP BY a.idAlumne, a.nom;
Explicació: Fem JOIN i agrupem. És bona pràctica agrupar també per idAlumne (clau primària) per si hi ha dos alumnes amb el mateix nom.

3.5.3. Quantitat d'exàmens per mòdul.

SQL
1
2
3
4
SELECT m.nom, COUNT(e.idExamen) AS num_examens
FROM Modul m
JOIN Examen e ON m.idModul = e.idModul
GROUP BY m.idModul, m.nom;

3.5.4. Alumnes amb mitjana superior a 5.

SQL
1
2
3
4
5
SELECT a.nom, AVG(e.nota) AS media
FROM Alumne a
JOIN Examen e ON a.idAlumne = e.idAlumne
GROUP BY a.idAlumne, a.nom
HAVING AVG(e.nota) > 5;
Explicació: WHERE filtra files abans d'agrupar. HAVING filtra grups després de calcular els agregats (com la mitjana).

3.5.5. Mòduls amb mitjana aprovada.

SQL
1
2
3
4
5
SELECT m.nom, AVG(e.nota) AS nota_media_modul
FROM Modul m
JOIN Examen e ON m.idModul = e.idModul
GROUP BY m.idModul, m.nom
HAVING AVG(e.nota) >= 5;

3.5.6. Quants mòduls imparteix cada professor.

SQL
1
2
3
4
SELECT p.nom, COUNT(d.idModul) AS quantitat_moduls
FROM Professor p
JOIN Docencia d ON p.idProfessor = d.idProfessor
GROUP BY p.idProfessor, p.nom;

3.6. Nivell 7: Consultes Avançades

3.6.1. Alumne amb la nota més alta (Subconsulta).

SQL
1
2
3
4
SELECT a.nom, e.nota
FROM Alumne a
JOIN Examen e ON a.idAlumne = e.idAlumne
WHERE e.nota = (SELECT MAX(nota) FROM Examen);
Explicació: La subconsulta (SELECT MAX...) obté el 10. Després busquem qui té aquesta nota.

3.6.2. Alumnes majors de la mitjana d'edat.

SQL
1
2
3
SELECT nom, edat
FROM Alumne
WHERE edat > (SELECT AVG(edat) FROM Alumne);
Explicació: Comparem l'edat de cada fila amb un valor escalar calculat independentment (la mitjana total).

3.6.3. Alumnes que han suspès algun examen.

SQL
1
2
3
4
SELECT DISTINCT a.nom
FROM Alumne a
JOIN Examen e ON a.idAlumne = e.idAlumne
WHERE e.nota < 5;
Explicació: Usem DISTINCT perquè si un alumne va suspendre 2 exàmens, no volem que el seu nom surti repetit.

3.6.4. Nom i Estat (CASE).

SQL
1
2
3
4
5
6
7
SELECT a.nom, e.nota,
CASE
    WHEN e.nota >= 5 THEN 'APROVAT'
    ELSE 'SUSPES'
END AS Estat
FROM Alumne a
JOIN Examen e ON a.idAlumne = e.idAlumne;
Explicació: CASE permet lògica condicional dins la selecció de columnes.

3.6.5. Repte: Professors i total d'exàmens corregits.

SQL
1
2
3
4
5
6
SELECT p.nom, COUNT(e.idExamen) AS examens_corregits
FROM Professor p
JOIN Docencia d ON p.idProfessor = d.idProfessor
JOIN Modul m ON d.idModul = m.idModul
LEFT JOIN Examen e ON m.idModul = e.idModul
GROUP BY p.idProfessor, p.nom;
Explicació: Aquesta consulta recorre tot l'esquema. Professor -> Docencia -> Modul -> Examen. Comptem els exàmens associats als mòduls que imparteix cada professor. Usem LEFT JOIN a l'últim pas per si hi ha un professor que imparteix un mòdul del qual encara no hi ha exàmens (apareixeria amb 0).

4. Secció 3. HQL vs Java

Hibernate Query Language (HQL) és un llenguatge de consulta orientat a objectes similar a SQL però dissenyat per treballar amb les entitats i les seves relacions en lloc de taules i columnes. A continuació, es mostren algunes diferències clau entre HQL i SQL:

Característica SQL HQL
Nivell d'abstracció Baix nivell, treballa directament amb taules i columnes. Alt nivell, treballa amb entitats i propietats.
Sintaxi Utilitza la sintaxi SQL estàndard. Sintaxi similar a SQL però adaptada a objectes.
Consultes SELECT, INSERT, UPDATE, DELETE. SELECT, FROM, WHERE, JOIN (no INSERT/UPDATE/DELETE directament).
Joins Basat en claus foranes i relacions de tabales. Basat en relacions d'objectes i associacions.
Agregacions Funcions d'agregació SQL (COUNT, AVG, etc.). Funcions d'agregació similars però aplicades a propietats d'entitats.
Portabilitat Depèn del dialecte SQL de la base de dades. Portabilitat entre diferents bases de dades suportades per Hibernate.
Mapeig No hi ha mapeig directe, es treballa amb l'esquema de la base de dades.
Resultats Retorna conjunts de resultats basats en files. Retorna objectes o col·leccions d'objectes.

Al SQL habitualment feiem les consultes i ja portem la informació filtrada. En HQL podem fer el mateix, però moltes vegades ens plantejarem el recuperar-ho tot i treballar en Java.

Exemple: Quants alumens hi han?

  1. Solució SQL/HQL:
Java
1
2
3
4
5
6
String SQL="SELECT COUNT(*) FROM Alumne;"
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(SQL);
if (rs.next()) {
    int numAlumnes = rs.getInt(1);
}
Java
Query query = session.createQuery("SELECT COUNT(a) FROM Alumne a");
int numAlumnes = (int) query.uniqueResult();
Java
1
2
3
Query query = session.createQuery("FROM Alumne");
List<Alumno> alumnes = query.list();
int numAlumnes = alumnes.size();

Comparar l'enfocament de base de dades (HQL/SQL) amb l'enfocament en memòria (Java) és una de les lliçons més valuoses per a un futur desenvolupador, ja que sovint es cau en l'error de portar-ho tot a memòria, causant problemes de rendiment greus.

Assumirem que les entitats (Alumno, Profesor, Modulo, Examen) estan correctament anotades amb les relacions JPA (@OneToMany, @ManyToOne, @ManyToMany).

Aquí tens una selecció d'exercicis representatius de diferents nivells de complexitat, resolts amb NamedQueries i comparats amb la lògica en Java.

4.1. Entitat Alumno amb NamedQueries

Java
import javax.persistence.*;
import java.util.Set;

@Entity
@Table(name = "Alumno")
@NamedQueries({
    // --- Nivel 2: Básicos ---
    @NamedQuery(name = "Alumno.findAll", 
                query = "SELECT a FROM Alumno a"), // Ej. 1

    @NamedQuery(name = "Alumno.findNombres", 
                query = "SELECT a.nombre, a.apellidos FROM Alumno a"), // Ej. 2

    @NamedQuery(name = "Alumno.findByEdadMayor", 
                query = "SELECT a FROM Alumno a WHERE a.edad >= :edad"), // Ej. 3

    @NamedQuery(name = "Alumno.findRepetidores", 
                query = "SELECT a FROM Alumno a WHERE a.repetidor = true"), // Ej. 4

    @NamedQuery(name = "Alumno.findAllOrderedByEdad", 
                query = "SELECT a FROM Alumno a ORDER BY a.edad DESC"), // Ej. 5

    // --- Nivel 3: Agregaciones ---
    @NamedQuery(name = "Alumno.countAll", 
                query = "SELECT COUNT(a) FROM Alumno a"), // Ej. 6

    @NamedQuery(name = "Alumno.avgEdad", 
                query = "SELECT AVG(a.edad) FROM Alumno a"), // Ej. 7

    @NamedQuery(name = "Alumno.sumEdad", 
                query = "SELECT SUM(a.edad) FROM Alumno a"), // Ej. 9

    // --- Nivel 5: Left Joins / Colecciones Vacías ---
    @NamedQuery(name = "Alumno.findAllWithNotes", 
                query = "SELECT a.nombre, e.nota FROM Alumno a LEFT JOIN a.examenes e"), // Ej. 16

    @NamedQuery(name = "Alumno.findSinExamenes", 
                query = "SELECT a.nombre FROM Alumno a WHERE a.examenes IS EMPTY"), // Ej. 17

    // --- Nivel 6: Group By ---
    @NamedQuery(name = "Alumno.avgNotas", 
                query = "SELECT a.nombre, AVG(e.nota) FROM Alumno a JOIN a.examenes e GROUP BY a.nombre"), // Ej. 21

    @NamedQuery(name = "Alumno.findAprobadosMedia", 
                query = "SELECT a.nombre, AVG(e.nota) FROM Alumno a JOIN a.examenes e GROUP BY a.nombre HAVING AVG(e.nota) > 5"), // Ej. 23

    // --- Nivel 7: Subconsultas ---
    @NamedQuery(name = "Alumno.findOlderThanAvg", 
                query = "SELECT a.nombre, a.edad FROM Alumno a WHERE a.edad > (SELECT AVG(a2.edad) FROM Alumno a2)"), // Ej. 27

    @NamedQuery(name = "Alumno.findSuspensos", 
                query = "SELECT DISTINCT e.alumno.nombre FROM Examen e WHERE e.nota < 5") // Ej. 28 (Mapeada aquí por lógica de negocio, aunque usa Examen)
})
public class Alumno {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long idAlumno;

    private String nombre;
    private String apellidos;
    private Integer edad;
    private Boolean repetidor;

    @OneToMany(mappedBy = "alumno")
    private Set<Examen> examenes;

    // Getters y Setters...
}

4.2. Entitat Examen amb NamedQueries

Java
import javax.persistence.*;

@Entity
@Table(name = "Examen")
@NamedQueries({
    // --- Nivel 3: Agregaciones ---
    @NamedQuery(name = "Examen.maxNota", 
                query = "SELECT MAX(e.nota) FROM Examen e"), // Ej. 8

    // --- Nivel 4: Navegación de Objetos (Path Navigation) ---
    @NamedQuery(name = "Examen.findAlumnoAndNota", 
                query = "SELECT e.alumno.nombre, e.nota FROM Examen e"), // Ej. 11

    @NamedQuery(name = "Examen.findModuloAndNotaOrdered", 
                query = "SELECT e.modulo.nombre, e.nota FROM Examen e ORDER BY e.nota DESC"), // Ej. 12

    @NamedQuery(name = "Examen.findAlumnosByModuloId", 
                query = "SELECT e.alumno.nombre, e.nota FROM Examen e WHERE e.modulo.idModulo = :idModulo"), // Ej. 14

    @NamedQuery(name = "Examen.findDetalleCompleto", 
                query = "SELECT e.alumno.nombre, e.modulo.nombre, e.nota FROM Examen e"), // Ej. 15

    // --- Nivel 5: Navegación Compleja ---
    @NamedQuery(name = "Examen.findByProfesorNombre", 
                query = "SELECT e FROM Examen e JOIN e.modulo m JOIN m.profesores p WHERE p.nombre = :nombreProf"), // Ej. 19

    // --- Nivel 6: Group By ---
    @NamedQuery(name = "Examen.avgNotaByAlumnoId", 
                query = "SELECT e.alumno.idAlumno, AVG(e.nota) FROM Examen e GROUP BY e.alumno.idAlumno"), // Ej. 20

    // --- Nivel 7: Complejas ---
    @NamedQuery(name = "Examen.findBestStudent", 
                query = "SELECT e.alumno.nombre, e.nota FROM Examen e WHERE e.nota = (SELECT MAX(e2.nota) FROM Examen e2)"), // Ej. 26

    @NamedQuery(name = "Examen.findEstadoBoletin", 
                query = "SELECT e.alumno.nombre, e.nota, CASE WHEN e.nota >= 5 THEN 'APROBADO' ELSE 'SUSPENSO' END FROM Examen e") // Ej. 29
})
public class Examen {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long idExamen;

    private Double nota;

    @ManyToOne
    @JoinColumn(name = "idAlumno")
    private Alumno alumno;

    @ManyToOne
    @JoinColumn(name = "idModul") // OJO: Tu tabla tiene la columna 'idModul'
    private Modulo modulo;

    // Getters y Setters...
}

4.3. Entitat Modulo amb NamedQueries

Java
import javax.persistence.*;
import java.util.Set;

@Entity
@Table(name = "Modulo")
@NamedQueries({
    // --- Nivel 5: Left Joins ---
    @NamedQuery(name = "Modulo.findAllWithProfesores", 
                query = "SELECT m.nombre, p.nombre FROM Modulo m LEFT JOIN m.profesores p"), // Ej. 18

    // --- Nivel 6: Agregaciones ---
    @NamedQuery(name = "Modulo.countExamenes", 
                query = "SELECT m.nombre, COUNT(e) FROM Modulo m JOIN m.examenes e GROUP BY m.nombre"), // Ej. 22

    @NamedQuery(name = "Modulo.findAprobadosMedia", 
                query = "SELECT m.nombre, AVG(e.nota) FROM Modulo m JOIN m.examenes e GROUP BY m.nombre HAVING AVG(e.nota) >= 5") // Ej. 24
})
public class Modulo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long idModulo;

    private String nombre;

    @ManyToMany(mappedBy = "modulos")
    private Set<Profesor> profesores;

    @OneToMany(mappedBy = "modulo")
    private Set<Examen> examenes;

    // Getters y Setters...
}

4.4. Entitat Profesor amb NamedQueries

Java
import javax.persistence.*;
import java.util.Set;

@Entity
@Table(name = "Profesor")
@NamedQueries({
    // --- Nivel 3: Contador básico ---
    @NamedQuery(name = "Profesor.countAll", 
                query = "SELECT COUNT(p) FROM Profesor p"), // Ej. 10

    // --- Nivel 4: Relación ManyToMany ---
    @NamedQuery(name = "Profesor.findWithModulos", 
                query = "SELECT p.nombre, m.nombre FROM Profesor p JOIN p.modulos m"), // Ej. 13

    // --- Nivel 6: Agregación ---
    @NamedQuery(name = "Profesor.countModulos", 
                query = "SELECT p.nombre, COUNT(m) FROM Profesor p JOIN p.modulos m GROUP BY p.nombre"), // Ej. 25

    // --- Nivel 7: El Reto Final (Relación Transitiva) ---
    // Profesor -> Modulos -> Examenes
    @NamedQuery(name = "Profesor.countExamenesCorregidos", 
                query = "SELECT p.nombre, COUNT(e) FROM Profesor p JOIN p.modulos m LEFT JOIN m.examenes e GROUP BY p.nombre") // Ej. 30
})
public class Profesor {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long idProfesor;

    private String nombre;

    @ManyToMany
    @JoinTable(
        name = "Docencia",
        joinColumns = @JoinColumn(name = "idProfesor"),
        inverseJoinColumns = @JoinColumn(name = "idModulo")
    )
    private Set<Modulo> modulos;

    // Getters y Setters...
}