La lliçó anterior va tancar el mòdul 3 amb una constatació incòmoda: l'API de CicloUrbana és completa, documentada i publicable, però quan el procés es reinicia tot desapareix. Les quatre estacions de Ribalta tornen a néixer del CarregadorEstacionsDemo, les bicicletes donades d'alta s'esfumen i els lloguers del dia es perden. Tot viu dins el ConcurrentHashMap d'EstacioRepositoriEnMemoria, que existeix mentre existeix la JVM. Aquest mòdul substitueix aquesta provisionalitat per persistència real sobre una base de dades relacional, i ho fa sense tocar ni un controlador, gràcies al fet que des de 02-01 vam amagar l'emmagatzematge darrere la interfície EstacioRepositori.
Abans d'escriure ni una sola anotació convé entendre el terreny. Aquesta lliçó és conceptual i respon les preguntes que gairebé ningú no es fa abans de començar i que tothom es fa quan alguna cosa falla: per què existeix un ORM, què significa exactament cadascuna de les quatre sigles que es barregen cada dia —JDBC, JPA, Hibernate, Spring Data JPA—, què és el context de persistència i per què una entitat pot estar «gestionada» o «separada», i què fa Spring Boot pel seu compte tan bon punt detecta l'starter. Sense aquesta base, JPA sembla màgia; amb ella, és una màquina previsible.
Contingut
- El desajust objecte-relacional
- Què és un ORM i quin problema resol realment
- Qui és qui: JDBC, JPA, Hibernate i Spring Data JPA
- Què aporta
spring-boot-starter-data-jpa EntityManager, unitat de persistència i context de persistència- Els quatre estats d'una entitat
- Què genera Spring Boot en detectar l'starter
- Alternatives a JPA dins l'ecosistema Spring
- El model de dades objectiu de CicloUrbana
- Errors Comuns i Consells
- Exercicis
- El desajust objecte-relacional
En Java, Estacio és un objecte: té identitat pròpia (dos objectes diferents encara que siguin iguals camp a camp), referències directes a altres objectes, herència, i viu en un graf que el recol·lector d'escombraries gestiona. En una base de dades relacional, una estació és una fila d'una taula: té identitat per clau primària, es relaciona amb altres files mitjançant claus foranes, no hereta de res i només existeix dins d'un conjunt de tuples.
Tots dos models són bons, però no encaixen. A aquesta col·lecció de desavinences se l'anomena desajust objecte-relacional (object-relational impedance mismatch), i té cinc cares concretes:
| Desavinença | En Java | En SQL | Conseqüència pràctica |
|---|---|---|---|
| Granularitat | Estacio conté un objecte Ubicacio amb latitud i longitud |
La taula estacions té dues columnes soltes |
Cal aplanar o expandir objectes |
| Identitat | == (referència) i equals() (valor) són coses diferents |
Només existeix la clau primària | Dos objectes poden representar la mateixa fila |
| Herència | Incidencia → IncidenciaBateria, IncidenciaVandalisme |
No hi ha herència entre taules | Cal triar una estratègia de mapatge |
| Associació | Referència dirigida: Bicicleta.estacio |
Clau forana simètrica, navegable en tots dos sentits | La bidireccionalitat s'ha de construir a mà |
| Navegació | bici.getEstacio().getNom(), salt a salt |
JOIN, tot d'una vegada |
Navegar objecte a objecte genera moltes consultes |
Un exemple de CicloUrbana ho fa tangible. Per obtenir el nom de l'estació d'una bicicleta, en Java el natural és:
En SQL el natural és el contrari: demanar-ho tot de cop.
SELECT b.matricula, e.nom
FROM bicicletes b
JOIN estacions e ON e.id = b.estacio_id
WHERE b.id = 42;La primera forma, executada dins d'un bucle sobre cent bicicletes, produeix cent consultes. Aquest és el germen del problema N+1 que veurem a 04-04. No és una errada de JPA: és el desajust traient el cap.
- Què és un ORM i quin problema resol realment
Un ORM (Object-Relational Mapping) és una capa que tradueix automàticament entre objectes i files. Escrius estacioRepositori.desar(estacio) i algú genera l'INSERT; llegeixes estacio.getBicicletes() i algú genera el SELECT.
Per valorar el que aporta, mira què costaria fer-ho a mà amb JDBC pur. Aquest seria el desar d'una estació sense ORM:
public Estacio desar(Estacio estacio) {
String sql = """
INSERT INTO estacions (nom, adreca, capacitat, latitud, longitud)
VALUES (?, ?, ?, ?, ?)
""";
try (Connection connexio = dataSource.getConnection();
PreparedStatement sentencia =
connexio.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
sentencia.setString(1, estacio.getNom());
sentencia.setString(2, estacio.getAdreca());
sentencia.setInt(3, estacio.getCapacitat());
sentencia.setDouble(4, estacio.getLatitud());
sentencia.setDouble(5, estacio.getLongitud());
sentencia.executeUpdate();
try (ResultSet claus = sentencia.getGeneratedKeys()) {
if (claus.next()) {
estacio.setId(claus.getLong(1));
}
}
return estacio;
} catch (SQLException e) {
throw new AccesDadesException("No s'ha pogut desar l'estació", e);
}
}Vint línies per a una taula de cinc columnes. Multiplica-ho per cercarPerId, cercarTotes, actualitzar, eliminar, i després per les sis taules del model. I encara falta el ResultSet → objecte, el tractament dels nuls, i el fet que cada ALTER TABLE obliga a revisar totes aquelles posicions de paràmetre numerades a mà.
El que un ORM aporta, ordenat per importància real:
- Elimina el codi repetitiu de mapatge. El 80 % d'aquest fragment desapareix.
- Gestiona la identitat i l'estat. Sap quins objectes ha carregat i quins han canviat, i genera l'
UPDATEnomés del que s'ha modificat. - Tradueix SQL específic de cada motor. El mateix codi funciona sobre H2 en desenvolupament i PostgreSQL en producció.
- Ofereix un llenguatge de consulta orientat a objectes (JPQL) que es valida contra el model, no contra cadenes.
- Automatitza la càrrega d'associacions i ofereix control sobre quan carregar-les.
I el que un ORM no aporta, perquè convé dir-ho des del principi:
- No t'eximeix de conèixer SQL. Quan alguna cosa va lenta, el diagnòstic es fa llegint el SQL generat.
- No converteix un model relacional dolent en un de bo.
- No és gratis: afegeix una capa amb regles pròpies que cal entendre (justament el que fa aquest mòdul).
- Qui és qui: JDBC, JPA, Hibernate i Spring Data JPA
Aquestes quatre peces s'anomenen cada dia com si fossin intercanviables i no ho són. Cadascuna viu en un nivell d'abstracció diferent i depèn de l'anterior.
| Peça | Què és | Qui la manté | Tipus d'artefacte | Exemple d'ús |
|---|---|---|---|---|
| JDBC | API estàndard de Java per parlar amb una base de dades via SQL | Especificació de Java SE | API + driver per motor | connection.prepareStatement(sql) |
| JPA (Jakarta Persistence) | Especificació: defineix anotacions, EntityManager, JPQL i el cicle de vida. No executa res |
Jakarta EE (abans JSR-338) | Interfícies i anotacions | @Entity, EntityManager.persist() |
| Hibernate | Implementació de JPA: el motor que genera i executa el SQL de debò | Red Hat | Llibreria (hibernate-core) |
És qui escriu l'INSERT |
| Spring Data JPA | Abstracció sobre JPA: genera implementacions de repositoris a partir d'interfícies | Spring (VMware/Broadcom) | Llibreria (spring-data-jpa) |
interface EstacioRepositori extends JpaRepository<...> |
La distinció crítica és JPA és una especificació, Hibernate és una implementació. JPA defineix que existeix @Entity i què ha de significar; Hibernate escriu el codi que fa que signifiqui això. Altres implementacions són EclipseLink i OpenJPA, però Spring Boot porta Hibernate per defecte i és l'elecció estàndard de facto.
graph TD
A["EstacioService (el teu codi de negoci)"] --> B["EstacioRepositori<br/>interfície Spring Data JPA"]
B --> C["SimpleJpaRepository<br/>implementació generada"]
C --> D["EntityManager<br/>API de JPA (especificació)"]
D --> E["Hibernate<br/>implementació de JPA"]
E --> F["JDBC + driver PostgreSQL"]
F --> G[("PostgreSQL 16")]
Cada fletxa cap avall és una traducció. estacioRepositori.findById(1L) es converteix en una crida a EntityManager.find(Estacio.class, 1L), que Hibernate tradueix a un SELECT ... WHERE id = ?, que el driver JDBC envia pel sòcol a PostgreSQL. Quan alguna cosa va malament, el diagnòstic consisteix a baixar per aquestes capes fins a trobar en quina es va trencar la traducció.
Un matís que estalvia confusió: Spring Data JPA no substitueix Hibernate, s'hi recolza. I Spring Data JPA no és Spring Data JDBC; comparteixen família i l'estil de repositoris, però per sota són projectes diferents (ho veurem a l'apartat 8).
- Què aporta
spring-boot-starter-data-jpa
spring-boot-starter-data-jpaCom tot starter (02-06), és un POM sense codi que arrossega un conjunt coherent de dependències amb versions ja compatibles entre si.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>Aquesta única línia afegeix al classpath:
| Dependència | Per a què serveix |
|---|---|
spring-boot-starter-jdbc |
DataSource, JdbcTemplate i el pool HikariCP |
hibernate-core |
El motor ORM: implementació de JPA |
jakarta.persistence-api |
Les anotacions i les interfícies de l'especificació |
spring-data-jpa |
Els repositoris i la seva generació automàtica |
spring-orm |
La integració de Spring amb JPA i el seu gestor de transaccions |
spring-aspects |
Suport AOP, base de @Transactional (04-07) |
jakarta.transaction-api |
L'API de transaccions |
El que no inclou —deliberadament— és el driver de la base de dades, perquè l'starter no pot saber a quin motor et connectaràs. Afegir H2 o PostgreSQL és el primer que farem a 04-02.
Pots comprovar l'arbre real al teu projecte:
EntityManager, unitat de persistència i context de persistència
EntityManager, unitat de persistència i context de persistènciaAquests tres termes són el vocabulari bàsic de JPA. Confondre'ls és la causa de la meitat de les preguntes de fòrum sobre «per què no es desen els meus canvis».
Unitat de persistència (persistence unit): la configuració completa d'un magatzem de dades —quin DataSource fer servir, quines classes són entitats, quin dialecte SQL, quines propietats d'Hibernate—. En una aplicació JPA clàssica es declarava a persistence.xml. En Spring Boot aquest fitxer no existeix: la unitat de persistència es construeix a partir d'application.yml i de l'escaneig automàtic d'entitats. És una de les coses que l'starter fa per tu.
EntityManager: la interfície central de JPA. És la teva porta d'entrada a la persistència. Les seves operacions essencials:
| Mètode | Què fa |
|---|---|
persist(entitat) |
Marca una entitat nova per ser inserida |
find(Classe, id) |
Recupera per clau primària (mira abans al context) |
getReference(Classe, id) |
Retorna un proxy mandrós sense anar a la base de dades |
merge(entitat) |
Reincorpora una entitat separada al context |
remove(entitat) |
Marca una entitat gestionada per esborrar |
flush() |
Força l'enviament del SQL pendent a la base de dades |
detach(entitat) / clear() |
Treu una entitat, o totes, del context |
createQuery(jpql) |
Crea una consulta JPQL |
Amb Spring Data JPA gairebé mai no el faràs servir directament, perquè els repositoris l'encapsulen. Però és allà sota, i entendre'n el comportament explica el dels repositoris.
Context de persistència (persistence context): és el concepte que de debò cal interioritzar. És una memòria cau de primer nivell que l'EntityManager manté amb totes les entitats que ha carregat o desat durant una unitat de treball. Té tres propietats que ho canvien tot:
- Garanteix identitat única: dins del mateix context, dues cerques de l'estació 1 retornen exactament el mateix objecte Java.
estacioA == estacioBéstrue. - Detecta canvis automàticament (dirty checking): desa una còpia de l'estat original; en fer flush compara i genera un
UPDATEnomés si alguna cosa ha canviat. Per això, sobre una entitat gestionada, no cal cridarsave()(ho desenvoluparem a 04-07). - Endarrereix l'escriptura (write-behind): acumula el SQL i l'envia a l'últim moment, cosa que permet agrupar-lo i ordenar-lo.
En una aplicació Spring típica, el context de persistència viu el que dura la transacció, normalment un mètode @Transactional del servei. Fora d'ell, les entitats queden separades. Aquest vincle entre context i transacció és l'eix del mòdul i el motiu de la lliçó 04-07.
- Els quatre estats d'una entitat
Tota instància d'una classe @Entity està sempre en un d'aquests quatre estats. Saber en quin és explica per què un canvi es desa o es perd.
| Estat | Té id? | És al context? | Se sincronitza amb la BD? |
|---|---|---|---|
| Transitori (transient/new) | Normalment no | No | No |
| Gestionat (managed/persistent) | Sí | Sí | Sí, automàticament |
| Separat (detached) | Sí | No | No |
| Eliminat (removed) | Sí | Sí, marcada per esborrar | S'esborrarà en fer flush |
stateDiagram-v2
[*] --> Transitori: new Estacio()
Transitori --> Gestionat: persist() / save()
Gestionat --> Separat: fi de transacció / detach() / clear()
Separat --> Gestionat: merge()
Gestionat --> Eliminat: remove() / delete()
Eliminat --> Gestionat: persist()
Eliminat --> [*]: commit (DELETE)
Separat --> [*]: recol·lector d'escombraries
Recorrem-los tots quatre amb una estació de Ribalta:
// 1. TRANSITORI: un objecte Java normal i corrent.
Estacio estacio = new Estacio("Plaça Major", "Plaça Major, 1", 24);
// Hibernate no sap que existeix. Si la JVM acaba, es perd.
// 2. GESTIONAT: a partir de save(), el context el vigila.
Estacio gestionada = estacioRepositori.save(estacio);
gestionada.setCapacitat(26);
// NO cal tornar a cridar save(): el dirty checking
// detectarà el canvi i emetrà l'UPDATE en fer commit.
// 3. SEPARAT: en acabar la transacció, el context es tanca.
// A partir d'aquí, els canvis ja no es propaguen sols:
gestionada.setCapacitat(30); // aquest canvi NO arriba a la base de dades
// 4. Per reincorporar-la cal fusionar-la:
Estacio reGestionada = estacioRepositori.save(gestionada); // fa mergeDues conseqüències que sorprenen tothom la primera vegada:
- El pas 2 no necessita
save()explícit. Moltíssim codi Spring el crida «per si de cas»; és inofensiu però revela que no s'ha entès el context de persistència. - El pas 3 sí que el necessita, i allà
save()no insereix: fa merge. La diferència es detalla a 04-05.
L'estat separat és el que produeix la temuda LazyInitializationException: intentar navegar una associació mandrosa d'una entitat el context de la qual ja s'ha tancat. Ho veurem a 04-04.
- Què genera Spring Boot en detectar l'starter
Tan bon punt spring-boot-starter-data-jpa és al classpath i hi ha un DataSource configurat, l'autoconfiguració de 02-06 es dispara. Les classes implicades són DataSourceAutoConfiguration, HibernateJpaAutoConfiguration i JpaRepositoriesAutoConfiguration, i entre totes registren aquests beans sense que escriguis ni una línia:
| Bean creat | Què fa | Condició |
|---|---|---|
DataSource (HikariCP) |
Pool de connexions | Hi ha spring.datasource.url o una BD incrustada |
EntityManagerFactory |
Fàbrica de l'EntityManager; construeix la unitat de persistència |
Hi ha DataSource + Hibernate |
EntityManager (proxy per transacció) |
La porta d'entrada a JPA | Hi ha EntityManagerFactory |
JpaTransactionManager |
Gestor de transaccions per a @Transactional |
Hi ha EntityManagerFactory |
JpaVendorAdapter |
Adapta Hibernate a l'abstracció de Spring | Hibernate al classpath |
| Repositoris | Un proxy per cada interfície que estengui Repository |
@EnableJpaRepositories implícit |
PlatformTransactionManager |
Base de la gestió transaccional (04-07) | — |
A més, escaneja automàticament les classes @Entity des del paquet de la classe anotada amb @SpringBootApplication cap avall. Com que CicloUrbanaApplication viu a com.ciclourbana, totes les entitats de com.ciclourbana.estacions, .bicicletes i .lloguers es detecten soles. És la mateixa regla de l'escaneig de components de 01-04, aplicada a entitats.
Pots verificar-ho amb l'informe d'autoconfiguració de 02-06:
A la secció Positive matches apareixeran entrades com:
HibernateJpaAutoConfiguration matched:
- @ConditionalOnClass found required classes 'jakarta.persistence.EntityManager',
'org.hibernate.SessionFactory' (OnClassCondition)
- @ConditionalOnBean found bean 'dataSource' (OnBeanCondition)
JpaRepositoriesAutoConfiguration#jpaRepositoriesFactoryBean matched:
- @ConditionalOnBean found bean 'entityManagerFactory'I si no configures un DataSource tenint l'starter, l'arrencada falla amb un missatge molt característic:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded
datasource could be configured.
Reason: Failed to determine a suitable driver classÉs exactament el problema que resol la lliçó següent.
- Alternatives a JPA dins l'ecosistema Spring
JPA és l'opció per defecte, no l'única ni sempre la millor. Conèixer el mapa evita l'error de forçar-la on no encaixa.
| Tecnologia | Nivell d'abstracció | Punts forts | Punts febles | Quan triar-la |
|---|---|---|---|---|
| JdbcTemplate | Baix: tu escrius el SQL | Control total, sense sorpreses, sense estat | Mapatge manual, codi repetitiu | Informes, consultes complexes, càrregues massives |
| Spring Data JDBC | Mitjà | Model simple, sense memòria cau ni càrrega mandrosa, molt predictible | Sense relacions mandroses ni herència | Models DDD amb agregats ben delimitats |
| Spring Data JPA | Alt | Productivitat, relacions, memòria cau, portabilitat | Corba d'aprenentatge, comportament implícit | CRUD de domini ric, el cas general |
| jOOQ | Baix, amb SQL tipat | SQL comprovat en compilació, perfecte per a consultes complexes | Requereix generació de codi; llicència comercial en BD propietàries | Aplicacions on el SQL és el centre |
| MyBatis | Baix-mitjà | SQL en XML o anotacions, mapatge flexible | Més configuració, menys automatisme | Migració d'aplicacions amb SQL heretat |
| R2DBC | Mitjà | Accés reactiu no bloquejant | Ecosistema menor, sense JPA possible | Aplicacions WebFlux amb molta concurrència |
Un criteri pràctic i honest: JPA brilla quan escrius i llegeixes agregats de domini; fa nosa quan fas informes. A CicloUrbana farem servir Spring Data JPA per a tot el CRUD d'estacions, bicicletes i lloguers, i a 04-06 veurem consultes natives per a casos com «estacions més properes», on el SQL específic de PostgreSQL guanya de llarg. No són excloents: conviuen al mateix projecte sobre el mateix DataSource.
- El model de dades objectiu de CicloUrbana
Aquest és el destí del mòdul. Sis taules que reflecteixen el negoci de la xarxa de Ribalta:
erDiagram
ESTACIONS ||--o{ BICICLETES : "acull"
ESTACIONS ||--o{ LLOGUERS : "origen"
ESTACIONS ||--o{ LLOGUERS : "destí"
BICICLETES ||--o{ LLOGUERS : "es lloga a"
BICICLETES ||--o{ INCIDENCIES : "acumula"
USUARIS ||--o{ LLOGUERS : "realitza"
TARIFES ||--o{ LLOGUERS : "s'aplica a"
ESTACIONS {
bigint id PK
varchar nom UK
varchar adreca
int capacitat
numeric latitud
numeric longitud
boolean activa
bigint versio
}
BICICLETES {
bigint id PK
varchar matricula UK
varchar estat
int nivell_bateria
bigint estacio_id FK
bigint versio
}
USUARIS {
bigint id PK
varchar correu UK
varchar nom
varchar tipus_tarifa
date data_alta
}
LLOGUERS {
bigint id PK
bigint usuari_id FK
bigint bicicleta_id FK
bigint estacio_origen_id FK
bigint estacio_desti_id FK
timestamp inici
timestamp fi
numeric import_total
}
TARIFES {
bigint id PK
varchar codi UK
numeric preu_minut
numeric import_minim
}
INCIDENCIES {
bigint id PK
bigint bicicleta_id FK
varchar tipus
varchar descripcio
timestamp reportada_el
}
Fixa't en tres detalls que anticipen lliçons concretes:
- La columna
versioaestacionsibicicletesés el bloqueig optimista de 04-03, el que substituirà elShallowEtagHeaderFilterde 03-03. lloguersté dues claus foranes aestacions(origen i destí): un cas onmappedByno n'hi ha prou i cal anomenar cada@JoinColumnexplícitament (04-04).import_totalipreu_minutsónnumeric, maidouble. El perquè és a 04-03, i és una de les regles menys negociables de tot el mòdul.
El recorregut del mòdul, recolzat en aquest model:
| Lliçó | Què afegeix a CicloUrbana |
|---|---|
| 04-02 | H2 en desenvolupament, PostgreSQL 16 en Docker, HikariCP afinat |
| 04-03 | Estacio, Bicicleta i Lloguer com a entitats amb @Version i auditoria |
| 04-04 | Les relacions del diagrama, LAZY i el problema N+1 |
| 04-05 | EstacioRepositori extends JpaRepository, paginació a l'API |
| 04-06 | Consultes derivades, JPQL, natives i projeccions |
| 04-07 | @Transactional als serveis, propagació i bloqueigs |
| 04-08 | Flyway: l'esquema com a codi versionat |
Errors Comuns i Consells
Creure que JPA és Hibernate. Són coses diferents: JPA és l'especificació, Hibernate la implementació. Importa quan apareix una anotació com @BatchSize, que és d'Hibernate, no de JPA: funciona, però et lliga al proveïdor. Comprova sempre el paquet de l'import: jakarta.persistence.* és estàndard; org.hibernate.annotations.* és específic.
Pensar que un ORM t'estalvia saber SQL. És exactament al revés: com més automàtica és la generació de SQL, més important és saber llegir-lo. Activa el registre de sentències des del primer dia (04-02) i acostuma't a mirar-lo.
Cridar save() sobre una entitat ja gestionada. No fa mal, però indica que no s'ha entès el dirty checking. Dins d'un mètode @Transactional, modificar una entitat carregada n'hi ha prou.
Oblidar que el context de persistència mor amb la transacció. És l'origen de la LazyInitializationException i dels «canvis que no es desen». Quan passi alguna cosa estranya, la primera pregunta ha de ser: en quin estat és aquesta entitat?
Triar JPA per a tot, informes inclosos. Una consulta d'agregació sobre un milió de lloguers no hauria de passar pel mapatge d'entitats. JdbcTemplate o una consulta nativa amb projecció és la resposta correcta (04-06).
Consell: dibuixa el model relacional abans d'anotar classes. El diagrama ER de dalt es va fer abans que les entitats, no després. Anotar sense un model pensat produeix esquemes que cal refer, i a 04-08 veuràs que refer un esquema en producció és car.
Consell: conserva la interfície EstacioRepositori. El mòdul sencer és la demostració que aïllar l'emmagatzematge darrere una interfície permet canviar de tecnologia sense tocar serveis ni controladors.
Exercicis
Exercici 1: classificar l'estat de les entitats
Donat el mètode següent d'un servei de CicloUrbana, indica en quin estat és la variable estacio a cada punt marcat i si el canvi de la línia final arriba a la base de dades. Justifica-ho.
@Transactional
public void reanomenarEstacio(Long id, String nouNom) {
Estacio estacio = new Estacio(); // (A)
estacio = estacioRepositori.findById(id) // (B)
.orElseThrow(() -> new RecursNoTrobatException("Estació", id));
estacio.setNom(nouNom); // (C)
} // (D) fi del mètodeExercici 2: triar la tecnologia d'accés a dades
Per a cada necessitat de l'ajuntament de Ribalta, tria entre JdbcTemplate, Spring Data JPA, jOOQ o R2DBC, i raona l'elecció en una frase.
- CRUD complet d'estacions amb validació i relacions amb bicicletes.
- Un informe mensual que creua lloguers, usuaris i tarifes amb sis
JOIN, tres subconsultes i funcions de finestra. - Carregar de cop 500.000 lectures de sensors de les bicicletes cada nit.
- Un tauler en temps real que manté 20.000 connexions obertes mostrant l'ocupació de les estacions.
Exercici 3: recórrer les capes
Explica, capa per capa, què passa quan EstacioService executa estacioRepositori.findById(3L) i l'estació «Parc del Riu» encara no és al context de persistència. Anomena les cinc capes implicades i què fa cadascuna.
Solucions
Solució 1.
- (A) Transitori.
new Estacio()crea un objecte Java normal; Hibernate no el coneix, no té id i res no el vigila. A més, aquesta línia és inútil: la referència se sobreescriu immediatament. És una olor de codi, no un error. - (B) Gestionat.
findByIdcarrega la fila i la incorpora al context de persistència de la transacció oberta per@Transactional. Hibernate desa una còpia del seu estat original per al dirty checking. - (C) Gestionat i «brut». L'objecte continua gestionat; el seu nom difereix ara de la còpia original. Encara no s'ha executat cap
UPDATE. - (D) El canvi SÍ que arriba a la base de dades. En acabar el mètode, el proxy transaccional fa commit; abans del commit s'executa un flush que compara estat actual i còpia original, detecta el nom modificat i emet
UPDATE estacions SET nom = ? WHERE id = ?. Després del commit el context es tanca i l'entitat passa a separada.
No cal cridar save(). És l'exemple canònic de dirty checking. Si el mètode no portés @Transactional, el context viuria només el que dura la crida al repositori, l'entitat tornaria separada i el canvi es perdria en silenci: el pitjor tipus d'errada, perquè no llança cap excepció.
Solució 2.
- Spring Data JPA. És el seu cas ideal: agregat de domini, relacions, validació, escriptura i lectura per identificador. La productivitat compensa de sobres l'abstracció.
- jOOQ (o
JdbcTemplateamb SQL natiu). SisJOIN, subconsultes i funcions de finestra no són expressables còmodament en JPQL, i forçar-ho produeix consultes il·legibles. jOOQ dona SQL tipat i comprovat en compilació;JdbcTemplateés l'alternativa sense dependències afegides. JdbcTemplateambbatchUpdate. Una càrrega massiva no ha de passar pel context de persistència: 500.000 entitats gestionades esgotarien la memòria i el dirty checking faria una feina inútil. Inserció per lots en JDBC directe.- R2DBC amb WebFlux. Vint mil connexions concurrents amb el model bloquejant de JDBC exigirien vint mil fils. L'accés reactiu no bloquejant està pensat exactament per a això. (Amb Java 21 els fils virtuals canvien part d'aquest càlcul, però R2DBC continua sent l'opció de manual.)
Solució 3.
EstacioServicecridaestacioRepositori.findById(3L)sobre la interfície. No sap res de JPA.- El proxy de Spring Data JPA —creat a l'arrencada, com veurem a 04-05— rep la crida i la delega a
SimpleJpaRepository, la implementació estàndard. SimpleJpaRepositorytradueix aentityManager.find(Estacio.class, 3L)i embolcalla el resultat enOptional.- L'
EntityManager(Hibernate) busca primer al context de persistència. Com que no hi és, genera el SQL adaptat al dialecte:select e1_0.id, e1_0.capacitat, ... from estacions e1_0 where e1_0.id=?. - JDBC i el driver de PostgreSQL demanen una connexió al pool HikariCP, envien la sentència preparada i retornen un
ResultSet.
De tornada, Hibernate construeix la instància d'Estacio, la registra al context de persistència amb una còpia del seu estat, i la retorna embolcallada en Optional. Si el servei tornés a demanar l'estació 3 dins la mateixa transacció, el pas 5 no es repetiria: la memòria cau de primer nivell retornaria el mateix objecte.
Conclusió
Ja tens el mapa complet del terreny. Saps que el desajust objecte-relacional no és un caprici sinó cinc desavinences concretes entre dos models igual de vàlids, i que un ORM existeix per absorbir-les a canvi d'introduir regles pròpies. Distingeixes amb precisió les quatre peces que es confonen cada dia: JDBC com a API de baix nivell, JPA com a especificació, Hibernate com a implementació que genera el SQL real, i Spring Data JPA com a capa que fabrica repositoris a partir d'interfícies. Coneixes les set dependències que arrossega spring-boot-starter-data-jpa i, sobretot, la que no arrossega: el driver. Entens què és el context de persistència i les seves tres propietats —identitat única, dirty checking i escriptura diferida—, que expliquen per què modificar una entitat gestionada n'hi ha prou per desar-la. Pots situar qualsevol instància en un dels quatre estats i predir si els seus canvis sobreviuran. Saps quins beans crea l'autoconfiguració tan bon punt detecta l'starter i com verificar-ho amb --debug. I tens criteri per saber quan JPA no és la resposta i convé JdbcTemplate, jOOQ o R2DBC.
També tens el destí a la vista: les sis taules del model de CicloUrbana, amb la seva columna versio per al bloqueig optimista, les seves dues claus foranes de lloguers a estacions i els seus imports en numeric.
Falta el primer de tot: no hi ha cap base de dades. Si afegeixes l'starter ara mateix i arrenques, l'aplicació fallarà amb «Failed to configure a DataSource». La lliçó 04-02, Configuració de Fonts de Dades, ho resol: muntarem H2 en memòria per a desenvolupament amb la seva consola web, aixecarem PostgreSQL 16 amb un docker-compose.yml propi de CicloUrbana, afinarem el pool HikariCP paràmetre a paràmetre amb criteris de dimensionament reals, decidirem quin valor de ddl-auto fer servir a cada entorn i per què open-in-view s'ha de desactivar des del primer dia, i deixarem el registre de SQL configurat per poder veure exactament què envia Hibernate a Ribalta.
Curs de Spring Boot
Mòdul 1: Introducció a Spring Boot
- Què és Spring Boot?
- Configuració del teu entorn de desenvolupament
- Creant la teva primera aplicació Spring Boot
- Entenent l'estructura del projecte
- L'arrencada i el cicle de vida de l'aplicació
Mòdul 2: Conceptes bàsics de Spring Boot
- Anotacions de Spring Boot
- Injecció de dependències a Spring Boot
- Àmbit i cicle de vida dels beans
- Configuració de Spring Boot
- Propietats de Spring Boot
- Autoconfiguració i starters per dins
Mòdul 3: Construint serveis web RESTful
- Introducció als serveis web RESTful
- Creant controladors REST
- Gestió dels mètodes HTTP
- Validació de dades d'entrada
- DTOs i mapatge entre capes
- Gestió d'excepcions a REST
- Documentar l'API amb OpenAPI
Mòdul 4: Accés a dades amb Spring Boot
- Introducció a Spring Data JPA
- Configuració de fonts de dades
- Creació d'entitats JPA
- Relacions entre entitats
- Ús de repositoris de Spring Data
- Mètodes de consulta a Spring Data JPA
- Transaccions i gestió de la persistència
- Migracions d'esquema amb Flyway
Mòdul 5: Seguretat a Spring Boot
- Introducció a Spring Security
- Configuració de Spring Security
- Autenticació i autorització d'usuaris
- Implementació d'autenticació JWT
- Seguretat a nivell de mètode i enduriment de l'API
Mòdul 6: Proves a Spring Boot
- Introducció a les proves
- Proves unitàries amb JUnit
- Simulació amb Mockito
- Proves d'integració
- Proves amb Testcontainers
Mòdul 7: Funcions avançades de Spring Boot
- Spring Boot Actuator
- Perfils de Spring Boot
- Tasques programades i execució asíncrona
- Spring Boot amb Docker
- Spring Boot i microserveis
- Comunicació entre serveis i tolerància a fallades
Mòdul 8: Desplegament d'aplicacions Spring Boot
- Introducció al desplegament
- Desplegant a Heroku
- Desplegant a AWS
- Desplegant a Kubernetes
- Integració i lliurament continus
Mòdul 9: Rendiment i monitoratge
- Ajust de rendiment
- Memòria cau amb Spring Cache
- Monitoratge amb Spring Boot Actuator
- Ús de Prometheus i Grafana
- Gestió de registres i logs
- Traçabilitat distribuïda
