Java Fortgeschrittenen Schulung – Kursbezug
Ziel dieses Kapitels: Sie können PreparedStatements + Transaktionen korrekt einsetzen und verstehen, wie Hibernate Entities mappt (inkl. Lazy Loading / N+1).
Worum geht’s?
- JDBC: Connection/Statement/ResultSet sauber schließen (try-with-resources).
- PreparedStatement: sicher (SQL Injection vermeiden) und sauber parametriert.
- Transaktionen: atomare Änderungen (
commit/rollback) – z.B. „Einschreibung + Plätze reduzieren“. - ORM/Hibernate: Entities, Fetching (Lazy/Eager) und typische Performance-Fallen (N+1).
Lehr-/Lernziele
- (LZ1) JDBC-Zugriffe robust implementieren (try-with-resources, PreparedStatement, ResultSet-Mapping).
- (LZ2) Transaktionen korrekt steuern (Auto-Commit aus, commit/rollback, Fehlerpfade).
- (LZ3) Hibernate/ORM-Grundprinzipien erklären und typische Fallen vermeiden (Entity, Session/EM, Lazy, N+1).
JDBC: sauberer Read mit PreparedStatement
import java.sql.*;
public class KursRepositoryJDBC {
public Kurs findeNachId(Connection con, long id) throws SQLException {
String sql = "SELECT id, titel, tage, basispreis FROM kurse WHERE id = ?";
try (PreparedStatement ps = con.prepareStatement(sql)) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return null;
return new Kurs(
rs.getLong("id"),
rs.getString("titel"),
rs.getInt("tage"),
rs.getDouble("basispreis")
);
}
}
}
public static record Kurs(long id, String titel, int tage, double basispreis) {}
}Transaktionen: commit/rollback richtig
import java.sql.*;
public class EinschreibungServiceJDBC {
public void einschreiben(Connection con, long kursId, String teilnehmerId) throws SQLException {
boolean vorherAutoCommit = con.getAutoCommit();
con.setAutoCommit(false);
try {
// 1) Einschreibung anlegen
try (PreparedStatement ps = con.prepareStatement(
"INSERT INTO einschreibungen(kurs_id, teilnehmer_id) VALUES(?, ?)"
)) {
ps.setLong(1, kursId);
ps.setString(2, teilnehmerId);
ps.executeUpdate();
}
// 2) Plätze reduzieren (Guard: nur wenn noch Plätze frei)
try (PreparedStatement ps = con.prepareStatement(
"UPDATE kurse SET plaetze = plaetze - 1 WHERE id = ? AND plaetze > 0"
)) {
ps.setLong(1, kursId);
int updated = ps.executeUpdate();
if (updated == 0) throw new SQLException("Keine Plätze mehr frei (oder Kurs nicht gefunden)");
}
con.commit();
} catch (SQLException ex) {
con.rollback();
throw ex;
} finally {
con.setAutoCommit(vorherAutoCommit);
}
}
}Repository-Pattern: DB-Details kapseln
import java.sql.*;
public class KursService {
private final KursRepositoryJDBC repo;
public KursService(KursRepositoryJDBC repo) {
this.repo = repo;
}
public String kursTitel(Connection con, long id) throws SQLException {
KursRepositoryJDBC.Kurs kurs = repo.findeNachId(con, id);
if (kurs == null) return "(Kurs nicht gefunden)";
return kurs.titel();
}
}Hibernate: Entity + Mapping
import jakarta.persistence.*;
@Entity
@Table(name = "kurse")
public class KursEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String titel;
private int tage;
@Column(name = "basispreis")
private double basispreis;
@Column(name = "plaetze")
private int plaetze;
protected KursEntity() {} // JPA braucht Default-Konstruktor
public KursEntity(String titel, int tage, double basispreis, int plaetze) {
this.titel = titel;
this.tage = tage;
this.basispreis = basispreis;
this.plaetze = plaetze;
}
public Long getId() { return id; }
public String getTitel() { return titel; }
public int getTage() { return tage; }
public double getBasispreis() { return basispreis; }
public int getPlaetze() { return plaetze; }
}Hibernate Usage: EntityManager (CRUD)
import jakarta.persistence.*;
public class KursRepositoryJPA {
private final EntityManager em;
public KursRepositoryJPA(EntityManager em) { this.em = em; }
public KursEntity finde(long id) {
return em.find(KursEntity.class, id);
}
public KursEntity speichern(KursEntity k) {
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
em.persist(k);
tx.commit();
return k;
} catch (RuntimeException ex) {
tx.rollback();
throw ex;
}
}
}Typische Falle: N+1 Queries (kurz verstehen)
// Idee (Pseudo): Kurse laden (1 Query)
// und dann pro Kurs die Einschreibungen lazy nachladen (N weitere Queries) => N+1
//
// Typische Gegenmaßnahmen:
// - JOIN FETCH
// - DTO-/Projection-Queries
// - Batch Fetching
// - Query-Tuning
// Beispiel (JPA, als Idee):
// SELECT k FROM KursEntity k JOIN FETCH k.einschreibungen WHERE k.tage = :tagePraxisaufgabe (Mini)
- Erstellen Sie
KursRepositoryJDBCmitfindeNachTitel(Connection, String)via PreparedStatement. - Bauen Sie
EinschreibungServiceJDBC, der 2 Statements in einer Transaktion ausführt: Insert Einschreibung + Update Plätze. - Optional: Skizzieren Sie eine
@Entity KursEntity(id, titel, plaetze).
Lösung anzeigen
import java.sql.*;
public class KursRepositoryJDBC {
public Kurs findeNachTitel(Connection con, String titel) throws SQLException {
String sql = "SELECT id, titel, plaetze FROM kurse WHERE titel = ?";
try (PreparedStatement ps = con.prepareStatement(sql)) {
ps.setString(1, titel);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return null;
return new Kurs(
rs.getLong("id"),
rs.getString("titel"),
rs.getInt("plaetze")
);
}
}
}
public static record Kurs(long id, String titel, int plaetze) {}
}import java.sql.*;
public class EinschreibungServiceJDBC {
public void einschreiben(Connection con, long kursId, String teilnehmerId) throws SQLException {
boolean vorherAutoCommit = con.getAutoCommit();
con.setAutoCommit(false);
try {
// 1) Einschreibung anlegen
try (PreparedStatement ps = con.prepareStatement(
"INSERT INTO einschreibungen(kurs_id, teilnehmer_id) VALUES(?, ?)"
)) {
ps.setLong(1, kursId);
ps.setString(2, teilnehmerId);
ps.executeUpdate();
}
// 2) Plätze reduzieren (Guard)
try (PreparedStatement ps = con.prepareStatement(
"UPDATE kurse SET plaetze = plaetze - 1 WHERE id = ? AND plaetze > 0"
)) {
ps.setLong(1, kursId);
int updated = ps.executeUpdate();
if (updated == 0) throw new SQLException("Keine Plätze mehr frei");
}
con.commit();
} catch (SQLException ex) {
con.rollback();
throw ex;
} finally {
con.setAutoCommit(vorherAutoCommit);
}
}
}import jakarta.persistence.*;
@Entity
@Table(name = "kurse")
class KursEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String titel;
@Column(name = "plaetze")
private int plaetze;
protected KursEntity() {}
public KursEntity(String titel, int plaetze) {
this.titel = titel;
this.plaetze = plaetze;
}
public Long getId() { return id; }
public String getTitel() { return titel; }
public int getPlaetze() { return plaetze; }
}Optional: typische Stolperfallen
- Leak: ResultSet/Statement/Connection nicht schließen → try-with-resources nutzen.
- SQL Injection: String-Konkatenation vermeiden → PreparedStatement.
- Transaktion vergessen: mehrere Updates ohne TX → inkonsistente Daten bei Fehlern.
- ORM-Mythen: ORM ersetzt kein SQL-Verständnis – es verschiebt nur die Abstraktionsebene.
Kurz-Takeaways
- JDBC: try-with-resources + PreparedStatement + Mapping.
- Transaktionen: AutoCommit aus → commit/rollback sauber.
- Hibernate: Entity/EntityManager verstehen, N+1 vermeiden.