SemaTrain Logo Ein Fachportal von SemaTrain

Datenbankanbindung mit JDBC & ORM (Hibernate)

Sie lernen JDBC sauber einzusetzen (PreparedStatement, Transaktionen) und verstehen, wann ORM/Hibernate Vorteile bringt – inklusive typischer Stolperfallen (N+1, Lazy Loading, Session-Lifecycle).

Hinweis: Beispiele sind bewusst kompakt. In Produktivprojekten kommen meist Connection-Pooling (z.B. HikariCP) und Framework-Integration (z.B. Spring) dazu.

Java Fortgeschrittenen Schulung – Kursbezug

Dieses Kapitel ist Teil des Lernpfads zur Java Fortgeschrittenen Schulung. Fokus: solider DB-Zugriff (JDBC) und produktives Mapping (ORM/Hibernate) – im SemaTrain-Trainingskontext.

Ziel dieses Kapitels: Sie können PreparedStatements + Transaktionen korrekt einsetzen und verstehen, wie Hibernate Entities mappt (inkl. Lazy Loading / N+1).

Worum geht’s?

Lehr-/Lernziele

Nach diesem Kapitel können Sie …

JDBC: sauberer Read mit PreparedStatement

Pattern: try-with-resources + Mapping in eine Domain-Klasse (SemaTrain: Trainings/Kurse).

JDBC Read: PreparedStatement + ResultSet-Mapping (Kurs) (Java)
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

Szenario: Einschreibung anlegen + Plätze reduzieren muss atomar sein.

JDBC-Transaktion: Insert + Update atomar (commit/rollback) (Java)
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

Service bleibt lesbar, SQL bleibt im Repository.

Service liest verständlich, Repository macht DB-Details (Java)
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

Minimal: @Entity + @Id + Spalten. (SemaTrain: Kurs mit Plätzen)

JPA/Hibernate Entity: KursEntity → Tabelle "kurse" (Java)
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)

Transaktion: begin/commit, sonst werden Änderungen nicht sicher gespeichert.

JPA/Hibernate: EntityManager + Transaktion (CRUD) (Java)
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)

Wenn Sie 1 Liste laden und pro Element „nochmal DB“ passiert, wird’s schnell langsam.

N+1: 1 + N Queries → Join Fetch / DTO / Batching (Java)
// 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 = :tage

Praxisaufgabe (Mini)

Beitrag zu den Lehr-/Lernzielen: LZ1 (JDBC) · LZ2 (TX) · LZ3 (ORM-Verständnis)

  1. Erstellen Sie KursRepositoryJDBC mit findeNachTitel(Connection, String) via PreparedStatement.
  2. Bauen Sie EinschreibungServiceJDBC, der 2 Statements in einer Transaktion ausführt: Insert Einschreibung + Update Plätze.
  3. Optional: Skizzieren Sie eine @Entity KursEntity (id, titel, plaetze).
Lösung anzeigen
Lösung (Teil 1): findByTitle → findeNachTitel (PreparedStatement + Mapping) (Java)
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) {}
}
Lösung (Teil 2): TX über 2 Statements (Insert + Update) (Java)
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);
    }
  }
}
Optional (Teil 3): Entity-Skizze als ORM-Startpunkt (Java)
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

Quiz: JDBC & ORM (Lehr-/Lernziele Check)

1. (LZ1) Warum nutzt man PreparedStatement statt String-Konkatenation?

2. (LZ2) Was ist ein typisches Vorgehen für mehrere DB-Statements in einer Transaktion?

3. (LZ3) Was beschreibt das N+1 Problem am besten?

4. (LZ3) Was ist eine Entity in Hibernate/JPA?

Praxisaufgabe

Mini-Projekt: „Einschreibung“ (JDBC + Transaktion im SemaTrain-Kurskontext)

Beitrag zu den Lehr-/Lernzielen: LZ1 (PreparedStatement + Mapping) · LZ2 (Transaktion commit/rollback) · LZ3 (ORM-Grundidee + typische Fallen)

Szenario: Ein Teilnehmer schreibt sich in einen Kurs ein. Das System muss zwei Dinge speichern: (1) Einschreibung anlegen und (2) verfügbare Plätze reduzieren. Das muss atomar passieren: bei Fehlern darf nichts halb gespeichert sein.

Lösung anzeigen
Teil 0: Mini-Schema (nur zur Orientierung) (Text)
-- Minimal-Schema (Beispiel, SQL)
CREATE TABLE kurse (
  id BIGINT PRIMARY KEY,
  titel VARCHAR(255) NOT NULL,
  plaetze INT NOT NULL,
  basispreis DECIMAL(10,2) NULL
);

CREATE TABLE einschreibungen (
  id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  kurs_id BIGINT NOT NULL,
  teilnehmer_id VARCHAR(64) NOT NULL,
  CONSTRAINT fk_einschreibung_kurs FOREIGN KEY (kurs_id) REFERENCES kurse(id)
);

-- Praxisnah (optional): pro Kurs/Teilnehmer nur eine Einschreibung
-- ALTER TABLE einschreibungen ADD CONSTRAINT uq_einschreibung UNIQUE (kurs_id, teilnehmer_id);
Teil 1: Repository (PreparedStatement + ResultSet-Mapping) (Java)
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) {}
}
Teil 2: Service (Transaktion über Insert + Update, commit/rollback) (Java)
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 aktualisiert = ps.executeUpdate();
        if (aktualisiert == 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);
    }
  }
}
Teil 3 (optional): Entity-Skizze (Hibernate/JPA) (Java)
// Optional (LZ3): ORM-Startpunkt als Entity-Skizze
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; }
}
Teil 4: N+1 kurz einordnen (Falle + Gegenmittel) (Text)
// Hinweis (Deutsch): N+1-Falle erkennen
// Wenn Sie eine Liste von Kursen laden (1 Query)
// und dann pro Kurs noch weitere Daten "lazy" nachladen (N Queries),
// entsteht das N+1-Problem.
// Gegenmittel: JOIN FETCH, DTO/Projection, Batch Fetching, Query-Tuning.
Teil 5: Erweiterungen (Praxis) (Text)
// Erweiterungen (praxisnah):
// - Connection-Pooling (z.B. HikariCP) statt neue Connections pro Request
// - Eindeutigkeit absichern: UNIQUE(kurs_id, teilnehmer_id)
// - Fachliche Fehler nach außen: z.B. KeinePlaetzeException statt SQLException
// - Logging (INFO/SEVERE) wie in Kapitel 03