LordLamer
  • Home
  • About Me
  • Familie
  • Knowledgeroot
  • Impressum
KEEP IN TOUCH

Posts in category Java

Spring JdbcClient mit KeyHolder: Auto-Increment-Werte zuverlässig abrufen

Jun29
2025
Written by lordlamer

Das Problem: Generierte IDs nach einem INSERT abrufen

Jeder Entwickler kennt das Szenario: Nach dem Einfügen eines neuen Datensatzes in eine Datenbank benötigen wir oft die automatisch generierte ID für weitere Operationen. Sei es für Logging, für die Rückgabe an den Client oder für nachfolgende Datenbankoperationen – der generierte Primärschlüssel ist essentiell.

Mit Spring Boot 3.2 wurde der neue JdbcClient eingeführt, der eine moderne, fluent API für JDBC-Operationen bietet. In diesem Artikel zeigen wir, wie Sie mit dem JdbcClient und KeyHolder zuverlässig auf Auto-Increment-Werte zugreifen können.

Beispiel-Szenario: User-Verwaltung

Nehmen wir eine einfache User-Tabelle als Beispiel:

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Und eine entsprechende Java-Klasse:

public class User {
    private Integer id;
    private String username;
    private String email;
    private LocalDateTime createdAt;
    
    // Konstruktoren, Getter und Setter...
}

Die Lösung: JdbcClient mit KeyHolder

Der bewährte und zuverlässige Ansatz verwendet den KeyHolder in Kombination mit dem modernen JdbcClient:

@Repository
public class UserRepository {
    
    private final JdbcClient jdbcClient;
    
    public UserRepository(JdbcClient jdbcClient) {
        this.jdbcClient = jdbcClient;
    }
    
    public int createUser(User user) {
        KeyHolder keyHolder = new GeneratedKeyHolder();

        int update = jdbcClient.sql("""
                INSERT INTO users (username, email, created_at) 
                VALUES (?, ?, ?)
                """)
                .params(
                        user.getUsername(),
                        user.getEmail(),
                        user.getCreatedAt()
                )
                .update(keyHolder);

        Assert.state(update == 1, "Failed to create user: " + user.getUsername());

        return keyHolder.getKey().intValue();
    }
}

Was passiert hier im Detail?

1. KeyHolder erstellen

KeyHolder keyHolder = new GeneratedKeyHolder();

Der GeneratedKeyHolder ist die Standard-Implementierung des KeyHolder-Interfaces und sammelt automatisch alle von der Datenbank generierten Schlüssel.

2. SQL mit Parametern ausführen

int update = jdbcClient.sql("""
        INSERT INTO users (username, email, created_at) 
        VALUES (?, ?, ?)
        """)
        .params(user.getUsername(), user.getEmail(), user.getCreatedAt())
        .update(keyHolder);

Der entscheidende Punkt ist der Aufruf von .update(keyHolder) anstatt dem einfachen .update(). Dadurch wird Spring angewiesen, die generierten Schlüssel zu erfassen.

3. Generierte ID abrufen

return keyHolder.getKey().intValue();

Nach der Ausführung enthält der KeyHolder die generierte ID, die wir als Integer zurückgeben können.

Vollständiges Service-Beispiel

@Service
@Transactional
public class UserService {
    
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public User createUser(String username, String email) {
        User user = new User();
        user.setUsername(username);
        user.setEmail(email);
        user.setCreatedAt(LocalDateTime.now());
        
        int generatedId = userRepository.createUser(user);
        user.setId(generatedId);
        
        log.info("Created user with ID: {} and username: {}", generatedId, username);
        
        return user;
    }
}

Error Handling und Best Practices

Robuste Fehlerbehandlung

public int createUserSafely(User user) {
    try {
        KeyHolder keyHolder = new GeneratedKeyHolder();
        
        int rowsAffected = jdbcClient.sql("""
                INSERT INTO users (username, email, created_at) 
                VALUES (?, ?, ?)
                """)
                .params(user.getUsername(), user.getEmail(), user.getCreatedAt())
                .update(keyHolder);
        
        if (rowsAffected != 1) {
            throw new DataAccessException("Expected 1 row to be affected, but was: " + rowsAffected) {};
        }
        
        Number key = keyHolder.getKey();
        if (key == null) {
            throw new DataAccessException("No generated key returned") {};
        }
        
        return key.intValue();
        
    } catch (DataAccessException e) {
        log.error("Failed to create user: {}", user.getUsername(), e);
        throw new ServiceException("Could not create user", e);
    }
}

Validierung der Eingangsdaten

public int createUser(User user) {
    // Validierung vor dem Insert
    if (user.getUsername() == null || user.getUsername().trim().isEmpty()) {
        throw new IllegalArgumentException("Username cannot be null or empty");
    }
    
    if (user.getEmail() == null || !isValidEmail(user.getEmail())) {
        throw new IllegalArgumentException("Valid email is required");
    }
    
    KeyHolder keyHolder = new GeneratedKeyHolder();
    
    int update = jdbcClient.sql("""
            INSERT INTO users (username, email, created_at) 
            VALUES (?, ?, ?)
            """)
            .params(user.getUsername(), user.getEmail(), user.getCreatedAt())
            .update(keyHolder);

    Assert.state(update == 1, "Failed to create user: " + user.getUsername());
    
    return keyHolder.getKey().intValue();
}

Batch-Operationen

Auch bei mehreren Einfügungen funktioniert der KeyHolder-Ansatz zuverlässig:

public List<Integer> createMultipleUsers(List<User> users) {
    List<Integer> generatedIds = new ArrayList<>();
    
    for (User user : users) {
        KeyHolder keyHolder = new GeneratedKeyHolder();
        
        jdbcClient.sql("""
                INSERT INTO users (username, email, created_at) 
                VALUES (?, ?, ?)
                """)
                .params(user.getUsername(), user.getEmail(), user.getCreatedAt())
                .update(keyHolder);
        
        generatedIds.add(keyHolder.getKey().intValue());
    }
    
    return generatedIds;
}

Datenbankkompatibilität

Der KeyHolder-Ansatz funktioniert zuverlässig mit allen gängigen Datenbanken:

  • MySQL: AUTO_INCREMENT wird automatisch erkannt
  • PostgreSQL: SERIAL und IDENTITY Spalten werden unterstützt
  • SQL Server: IDENTITY Spalten funktionieren out-of-the-box
  • Oracle: SEQUENCE-basierte Auto-Increment wird unterstützt

Testing

Unit Test mit H2 In-Memory-Datenbank

@DataJdbcTest
class UserRepositoryTest {
    
    @Autowired
    private JdbcClient jdbcClient;
    
    private UserRepository userRepository;
    
    @BeforeEach
    void setUp() {
        userRepository = new UserRepository(jdbcClient);
    }
    
    @Test
    void shouldReturnGeneratedIdWhenCreatingUser() {
        // Given
        User user = new User();
        user.setUsername("testuser");
        user.setEmail("test@example.com");
        user.setCreatedAt(LocalDateTime.now());
        
        // When
        int generatedId = userRepository.createUser(user);
        
        // Then
        assertThat(generatedId).isGreaterThan(0);
    }
    
    @Test
    void shouldCreateMultipleUsersWithUniqueIds() {
        // Given
        User user1 = createTestUser("user1", "user1@example.com");
        User user2 = createTestUser("user2", "user2@example.com");
        
        // When
        int id1 = userRepository.createUser(user1);
        int id2 = userRepository.createUser(user2);
        
        // Then
        assertThat(id1).isNotEqualTo(id2);
        assertThat(id2).isGreaterThan(id1);
    }
    
    private User createTestUser(String username, String email) {
        User user = new User();
        user.setUsername(username);
        user.setEmail(email);
        user.setCreatedAt(LocalDateTime.now());
        return user;
    }
}

Warum KeyHolder der zuverlässige Standard ist

Nach ausgiebigen Tests verschiedener Ansätze hat sich der KeyHolder-Pattern als der stabilste und konsistenteste Weg erwiesen, um mit Auto-Increment-Werten zu arbeiten:

Vorteile des KeyHolder-Ansatzes:

  • Zuverlässigkeit: Funktioniert konsistent über alle Datenbanktypen hinweg
  • Explizite Kontrolle: Klare Trennung zwischen Insert-Operation und Key-Abruf
  • Fehlerbehandlung: Einfache Überprüfung, ob ein Schlüssel generiert wurde
  • Flexibilität: Unterstützt auch Szenarien mit mehreren generierten Schlüsseln
  • Bewährt: Lange etablierter Standard in der Spring-Community

Fazit

Der Spring JdbcClient in Kombination mit dem KeyHolder bietet eine moderne, saubere und zuverlässige Lösung für den Umgang mit automatisch generierten Datenbankschlüsseln. Dieser Ansatz sollte in allen neuen Spring-Projekten verwendet werden, wo Auto-Increment-Werte benötigt werden.

Die wichtigsten Punkte zum Mitnehmen:

  1. Verwenden Sie immer einen KeyHolder beim Einfügen von Datensätzen mit Auto-Increment
  2. Prüfen Sie die Anzahl der betroffenen Zeilen für zusätzliche Sicherheit
  3. Implementieren Sie robuste Fehlerbehandlung für Produktionsumgebungen
  4. Der KeyHolder-Ansatz ist datenbankagnostisch und funktioniert überall

Mit diesem Pattern können Sie Auto-Increment-Werte zuverlässig und performant in Ihren Spring-Anwendungen handhaben – eine solide Grundlage für robuste Datenbankoperationen.

Posted in Software Architektur

Knowledgeroot startet neu mit Java

Jan01
2025
Written by lordlamer

Nach 20 Jahren PHP-Entwicklung ist es Zeit für Veränderung. Ich habe mich entschieden, Knowledgeroot komplett in Java neu zu entwickeln. Das bewährte System zur Wissensverwaltung erhält damit eine moderne technische Basis, während die Funktionalität erhalten bleibt.

Die Entscheidung ist mir nicht leicht gefallen, aber die Vorteile einer Neuentwicklung überwiegen. Nutzer von Knowledgeroot werden sich in der neuen Version sofort zurechtfinden – die bewährten Funktionen wie Baumstruktur, Rechteverwaltung und das vertraute Layout bleiben bestehen. Was sich ändert, ist nur das technische Fundament.

Die bisherige PHP-Version wird unter www.knowledgeroot.org archiviert und bleibt für bestehende Installationen verfügbar. Die neue Entwicklung findet komplett offen auf GitHub statt.

Posted in Knowledgeroot

Spring Boot – App Deployment on Linux

May13
2016
Written by lordlamer

Spring Boot bietet die Möglichkeit seine erstellte Jar-Datei auch als Selbstausführenden Linux-Dienst zur Verfügung zu stellen. Das heißt, die erstellte Jar-Datei lässt sich direkt als Linux-Dienst nutzen.

Um das Feature zu nutzen müsst ihr pom.xml wie folgt anpassen:

1
2
3
4
5
6
7
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <executable>true</executable>
    </configuration>
</plugin>

Nach einem Package erstellen können wir die Datei auf unser Zielsystem deployen. Ein Link in die entsprechenden Runlevels und schon können wir die Spring Boot App als Linux Dienst laufen lassen.

Weitere Informationen dazu hier:
http://docs.spring.io/spring-boot/docs/current/reference/html/deployment-install.html

SLF4J mit Logback und Liquibase

May12
2016
Written by lordlamer

In einem aktuellen Projekt verwende ich SLF4J mit Logback und möchte einige Logs in die Datenbank schreiben lassen. Dafür liefert Logback auch geeignete SQL Statements. Da ich aber in meinem Projekt mit Liquibase unterwegs bin wollte ich das gerne mit in die DB-Changelog mit aufnehmen. Das Ergebnis dazu möchte ich hier teilen:

Changeset
<changeSet id="1" author="fhabermann" runAlways="true">
        <validCheckSum>any</validCheckSum>
        <preConditions onFail="MARK_RAN">
            <not>
                <tableExists tableName="logging_event"/>
            </not>
        </preConditions>
        <createTable tableName="logging_event">
            <column name="timestmp" type="BIGINT">
                <constraints nullable="false" />
            </column>
            <column name="formatted_message" type="TEXT">
                <constraints nullable="false" />
            </column>
            <column name="logger_name" type="VARCHAR(255)">
                <constraints nullable="false" />
            </column>
            <column name="level_string" type="VARCHAR(255)">
                <constraints nullable="false" />
            </column>
            <column name="thread_name" type="VARCHAR(255)">
                <constraints nullable="true" />
            </column>
            <column name="reference_flag" type="SMALLINT">
                <constraints nullable="true" />
            </column>
            <column name="arg0" type="VARCHAR(255)">
                <constraints nullable="true" />
            </column>
            <column name="arg1" type="VARCHAR(255)">
                <constraints nullable="true" />
            </column>
            <column name="arg2" type="VARCHAR(255)">
                <constraints nullable="true" />
            </column>
            <column name="arg3" type="VARCHAR(255)">
                <constraints nullable="true" />
            </column>
            <column name="caller_filename" type="VARCHAR(255)">
                <constraints nullable="false" />
            </column>
            <column name="caller_class" type="VARCHAR(255)">
                <constraints nullable="false" />
            </column>
            <column name="caller_method" type="VARCHAR(255)">
                <constraints nullable="false" />
            </column>
            <column name="caller_line" type="VARCHAR(4)">
                <constraints nullable="false" />
            </column>
            <column name="event_id" type="BIGINT" autoIncrement="true" >
                <constraints nullable="false" primaryKey="true" />
            </column>
        </createTable>
    </changeSet>
    
    <changeSet id="2" author="fhabermann"  runAlways="true">
        <validCheckSum>any</validCheckSum>
        <preConditions onFail="MARK_RAN">
            <not>
                <tableExists tableName="logging_event_exception"/>
            </not>
        </preConditions>
        <createTable tableName="logging_event_exception">
            <column name="event_id" type="BIGINT"><constraints nullable="false" /></column>
            <column name="i" type="SMALLINT"><constraints nullable="false" /></column>
            <column name="trace_line" type="VARCHAR(255)"><constraints nullable="false" /></column>
        </createTable>
        <addPrimaryKey tableName="logging_event_exception" columnNames="event_id,i"/>
        
        <addForeignKeyConstraint baseColumnNames="event_id"
            baseTableName="logging_event_exception"
            constraintName="logging_event_exception_ibfk_1"
            onDelete="CASCADE"
            onUpdate="RESTRICT"
            referencedColumnNames="event_id"
            referencedTableName="logging_event"/>
            
    </changeSet>
    
    <changeSet id="3" author="fhabermann"  runAlways="true">
        <validCheckSum>any</validCheckSum>
        <preConditions onFail="MARK_RAN">
            <not>
                <tableExists tableName="logging_event_property"/>
            </not>
        </preConditions>
        <createTable tableName="logging_event_property">
            <column name="event_id" type="BIGINT"><constraints nullable="false" /></column>
            <column name="mapped_key" type="VARCHAR(255)"><constraints nullable="false" /></column>
            <column name="mapped_value" type="TEXT"><constraints nullable="true" /></column>
        </createTable>
        <addPrimaryKey tableName="logging_event_property" columnNames="event_id,mapped_key"/>
        
        <addForeignKeyConstraint baseColumnNames="event_id"
            baseTableName="logging_event_property"
            constraintName="logging_event_property_ibfk_1"
            onDelete="CASCADE"
            onUpdate="RESTRICT"
            referencedColumnNames="event_id"
            referencedTableName="logging_event"/>
    </changeSet>

Java XSL Transformation

May12
2016
Written by lordlamer

Für ein aktuelles Projekt in Java benötigte ich eine Funktion/Klasse die es mir ermöglicht XML Code mittels XSL zu Transformieren. Dazu gibt es auch einige Beispiele im Netz die aber meist fertige Dateien von der Festplatte einlesen und Transformieren.

Hier dazu mein Beispiel welches nur mit Strings umgeht:

import java.io.StringReader;
import java.io.StringWriter;import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;class XslTransformer {
/**
* transform given xml with given xsl
*
* @param xml
* @param xsl
* @return
* @throws TransformerException
*/
public static String transform(String xml, String xsl) throws TransformerException {
StringReader srXml = new StringReader(xml);
StringReader srXsl = new StringReader(xsl);
StringWriter result = new StringWriter();TransformerFactory tFactory = TransformerFactory.newInstance();
Transformer transformer = tFactory.newTransformer(
new javax.xml.transform.stream.StreamSource(srXsl));transformer.transform(
new javax.xml.transform.stream.StreamSource(srXml),
new javax.xml.transform.stream.StreamResult(result));

return result.toString();
}

}

Community

  • Forum
  • GitHub
  • Knowledgeroot
  • YouTube

Categories

  • bsd (1)
  • citrix (3)
  • Company (27)
  • Debian (11)
  • docker (1)
  • Familie (75)
  • Geocaching (2)
  • Hausbau (41)
  • IPv6 (5)
  • Java (5)
  • klettern (10)
  • Knowledgeroot (16)
  • Linux (12)
  • LUG Balista (1)
  • misc (22)
  • mysql (1)
  • netscreen (2)
  • postgresql (1)
  • sap (4)
  • Software Architektur (3)
  • solr (2)
  • vim (2)

EvoLve theme by Theme4Press  •  Powered by WordPress LordLamer
Frank Habermann

We use cookies to ensure that we give you the best experience on our website. If you continue to use this site we will assume that you are happy with it.