Skip to main content
Dat 2. semester
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Database med Javalin

Database

I vores webapplikation bruger vi PostgreSQL som database.

Java kommunikerer med databasen gennem JDBC.

Vi bruger HikariCP som connection pool, så applikationen kan genbruge databaseforbindelser.

Når applikationen er bygget op med services og mappere, ser flowet typisk sådan ud:

Controller
Service
Mapper
Connection pool
PostgreSQL

1. Dependencies

Når projektet skal bruge PostgreSQL og HikariCP, skal de tilføjes til pom.xml.

PostgreSQL-driveren gør det muligt for Java at kommunikere med PostgreSQL.

HikariCP håndterer connection poolen.

Brug de versionsnumre, der er angivet i semesterets setup/dependency-side:

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>VERSION</version>
</dependency>

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>VERSION</version>
</dependency>

Efter ændringer i pom.xml skal Maven hente dependencies.

2. Connection pool

En databaseforbindelse er en ressource, som skal håndteres ordentligt.

I stedet for at oprette en helt ny forbindelse hver gang vi skal udføre en SQL-kommando, bruger vi en connection pool.

Connection poolen holder et antal forbindelser klar, som kan lånes og afleveres igen.

I vores setup bruger vi HikariCP.

Et enkelt eksempel på konfiguration:

HikariConfig config = new HikariConfig();

config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydatabase");
config.setUsername("postgres");
config.setPassword("postgres");

HikariDataSource dataSource = new HikariDataSource(config);

I den færdige applikation samler vi denne konfiguration i en ConnectionPool-klasse.

3. Hent en connection

Når en mapper skal tale med databasen, henter den en forbindelse fra connection poolen:

try (Connection connection = connectionPool.getConnection()) {

    // brug connection

}

try-with-resources sørger for, at forbindelsen bliver lukket igen.

Det betyder ikke, at hele connection poolen nødvendigvis lukkes. Connectionen afleveres tilbage til poolen.

4. ConnectionPool-klassen

En connection pool kan eksempelvis være samlet i en klasse:

public class ConnectionPool {

    private static HikariDataSource dataSource;

    public static ConnectionPool getInstance(
            String user,
            String password,
            String url,
            String db) {

        // initialisering af pool

        return new ConnectionPool();
    }

    public Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }
}

Vores konkrete ConnectionPool indeholder mere konfiguration.

Det vigtige for mapperen er, at den kan gøre:

Connection connection = connectionPool.getConnection();

5. Databaseforbindelse i Main

Et typisk setup kan se sådan ud:

private static final String USER = "postgres";
private static final String PASSWORD = "postgres";
private static final String URL =
        "jdbc:postgresql://localhost:5432/%s?currentSchema=public";
private static final String DB = "mydatabase";

private static final ConnectionPool connectionPool =
        ConnectionPool.getInstance(USER, PASSWORD, URL, DB);

Her er:

localhost

den computer, hvor PostgreSQL kører.

5432

er PostgreSQLs port.

mydatabase

er navnet på databasen.

6. Mapper

En mapper har ansvaret for at oversætte mellem Java-objekter og database-data.

For eksempel:

User object
UserMapper
users table

En mapper kan indeholde metoder som:

getAllUsers()
getUserById(int id)
createUser(User user)
updateUser(User user)
deleteUser(int id)

Controlleren bør ikke indeholde SQL.

SQL hører hjemme i mapperen.

7. SELECT

En simpel SQL-query:

SELECT *
FROM users;

I Java:

String sql = "SELECT * FROM users";

Opret en connection:

try (Connection connection = connectionPool.getConnection()) {

}

Opret en PreparedStatement:

try (PreparedStatement ps = connection.prepareStatement(sql)) {

}

Kør query’en:

ResultSet rs = ps.executeQuery();

8. ResultSet

ResultSet indeholder de rækker, som databasen returnerede.

Vi kan gå gennem rækkerne med rs.next():

while (rs.next()) {

    String username = rs.getString("username");

}

Hvis kolonnen er et heltal:

int id = rs.getInt("id");

Hvis kolonnen er en dato:

LocalDate date = rs.getObject("date", LocalDate.class);

9. Map database row til entity

Mapperen kan omdanne en database-række til et Java-objekt:

while (rs.next()) {

    String username = rs.getString("username");
    String password = rs.getString("password");

    User user = new User(username, password);

}

Hvis vi har flere rækker, samler vi objekterne i en liste:

List<User> users = new ArrayList<>();

while (rs.next()) {

    String username = rs.getString("username");
    String password = rs.getString("password");

    User user = new User(username, password);
    users.add(user);
}

10. Et komplet SELECT-eksempel

public List<User> getAllUsers(ConnectionPool connectionPool) {

    List<User> users = new ArrayList<>();

    String sql = """
            SELECT username, password
            FROM users
            """;

    try (Connection connection = connectionPool.getConnection();
         PreparedStatement ps = connection.prepareStatement(sql);
         ResultSet rs = ps.executeQuery()) {

        while (rs.next()) {

            String username = rs.getString("username");
            String password = rs.getString("password");

            User user = new User(username, password);
            users.add(user);
        }

    } catch (SQLException e) {
        throw new RuntimeException(e);
    }

    return users;
}

I jeres egentlige projekt bør databasefejl håndteres med den exception-struktur, I har valgt.

11. PreparedStatement

Hvis SQL’en indeholder værdier fra brugeren, skal vi ikke bygge SQL’en ved at sætte strings sammen.

Undgå:

String sql =
        "SELECT * FROM users WHERE username = '" + username + "'";

Brug i stedet en parameter:

String sql =
        "SELECT * FROM users WHERE username = ?";

Sæt parameteren:

PreparedStatement ps =
        connection.prepareStatement(sql);

ps.setString(1, username);

Kør query’en:

ResultSet rs = ps.executeQuery();

? er en placeholder.

Tallet 1 betyder den første parameter.

12. SELECT med parameter

public User getUserByUsername(
        String username,
        ConnectionPool connectionPool) {

    String sql = """
            SELECT username, password
            FROM users
            WHERE username = ?
            """;

    try (Connection connection = connectionPool.getConnection();
         PreparedStatement ps = connection.prepareStatement(sql)) {

        ps.setString(1, username);

        try (ResultSet rs = ps.executeQuery()) {

            if (rs.next()) {

                String password = rs.getString("password");

                return new User(username, password);
            }
        }

    } catch (SQLException e) {
        throw new RuntimeException(e);
    }

    return null;
}

13. INSERT

Når vi indsætter data:

INSERT INTO users (username, password)
VALUES (?, ?);

I Java:

String sql = """
        INSERT INTO users (username, password)
        VALUES (?, ?)
        """;

Sæt værdierne:

ps.setString(1, user.getUsername());
ps.setString(2, user.getPassword());

Kør:

ps.executeUpdate();

executeUpdate() bruges til SQL, der ændrer data.

14. INSERT-eksempel

public void createUser(
        User user,
        ConnectionPool connectionPool) {

    String sql = """
            INSERT INTO users (username, password)
            VALUES (?, ?)
            """;

    try (Connection connection = connectionPool.getConnection();
         PreparedStatement ps = connection.prepareStatement(sql)) {

        ps.setString(1, user.getUsername());
        ps.setString(2, user.getPassword());

        ps.executeUpdate();

    } catch (SQLException e) {
        throw new RuntimeException(e);
    }
}

15. UPDATE

Et eksempel:

UPDATE users
SET password = ?
WHERE username = ?;

Java:

String sql = """
        UPDATE users
        SET password = ?
        WHERE username = ?
        """;

try (PreparedStatement ps = connection.prepareStatement(sql)) {

    ps.setString(1, newPassword);
    ps.setString(2, username);

    ps.executeUpdate();
}

Husk WHERE.

Uden WHERE risikerer du at ændre alle rækker.

16. DELETE

Et eksempel:

DELETE FROM users
WHERE username = ?;

Java:

String sql = """
        DELETE FROM users
        WHERE username = ?
        """;

try (PreparedStatement ps = connection.prepareStatement(sql)) {

    ps.setString(1, username);

    ps.executeUpdate();
}

Igen: vær meget opmærksom på WHERE.

17. Hvorfor executeQuery() og executeUpdate()?

Brug:

executeQuery()

når du forventer et ResultSet.

Typisk:

SELECT

Brug:

executeUpdate()

når du ændrer databasen.

Typisk:

INSERT
UPDATE
DELETE

18. Hent genereret ID

Hvis databasen selv genererer et ID, kan vi bede JDBC om det genererede ID.

SQL:

INSERT INTO users (username, password)
VALUES (?, ?);

Java:

PreparedStatement ps =
        connection.prepareStatement(
                sql,
                Statement.RETURN_GENERATED_KEYS);

Efter executeUpdate():

ps.executeUpdate();

try (ResultSet keys = ps.getGeneratedKeys()) {

    if (keys.next()) {
        int id = keys.getInt(1);
    }
}

Det er nyttigt, når Java-objektet skal kende det ID, databasen har tildelt.

19. Mapper og Service

Mapperen arbejder med databasen.

Servicen arbejder med programmets logik.

Eksempel:

public class UserService {

    private UserMapper userMapper;

    public User login(String username, String password) {

        User user = userMapper.getUserByUsername(username);

        if (user != null &&
            user.getPassword().equals(password)) {

            return user;
        }

        return null;
    }
}

Mapperen:

public User getUserByUsername(String username) {
    // SQL
}

Controlleren:

public static void login(Context ctx) {

    String username = ctx.formParam("username");
    String password = ctx.formParam("password");

    User user = userService.login(username, password);

    // HTTP response
}

Dermed bliver ansvaret fordelt:

Controller
    HTTP
Service
    forretningslogik
Mapper
    SQL
Database

20. Exceptions

Databaseoperationer kan fejle.

JDBC-metoder kan blandt andet kaste SQLException.

Eksempel:

try {
    // databasekode
} catch (SQLException e) {
    // håndter databasefejl
}

En databasefejl er ikke nødvendigvis brugerens fejl.

Hvis databasen ikke svarer, skal brugeren ikke have en besked som:

Du har skrevet forkert password.

Programmet skal i stedet håndtere den uventede fejl korrekt.

I større projekter kan man bruge en egen DatabaseException:

catch (SQLException e) {
    throw new DatabaseException(
            e,
            "Could not get users from database");
}

Det gør det muligt at skelne mellem databasefejl og andre typer fejl.

21. Connection skal lukkes

Brug try-with-resources:

try (Connection connection = connectionPool.getConnection()) {

    // databasearbejde

}

Det samme gælder PreparedStatement og ResultSet:

try (Connection connection = connectionPool.getConnection();
     PreparedStatement ps = connection.prepareStatement(sql);
     ResultSet rs = ps.executeQuery()) {

    // brug rs

}

Det er en god vane at lade Java håndtere lukningen automatisk.

22. SQL med JOIN

Mapperen kan selvfølgelig også bruge JOINs.

SELECT users.username, tasks.title
FROM users
JOIN tasks
    ON users.id = tasks.user_id;

Resultatet kan læses fra ResultSet:

String username = rs.getString("username");
String title = rs.getString("title");

Hvis query’en bliver mere kompleks, kan det være relevant at mappe resultatet til en DTO i stedet for direkte til en entity.

23. Views

En database kan indeholde views.

Et view kan samle en kompleks query:

CREATE VIEW user_tasks AS
SELECT users.username, tasks.title
FROM users
JOIN tasks
    ON users.id = tasks.user_id;

Mapperen kan derefter læse fra viewet:

SELECT *
FROM user_tasks;

Views er stadig en del af databasen. Java behøver ikke kende den underliggende kompleksitet.

24. Test databasekoden

Databasekode er typisk integrationstest-stof.

En integrationstest kan for eksempel kontrollere, at:

Java
Mapper
PostgreSQL

faktisk fungerer.

En unit test af UserService behøver derimod ikke nødvendigvis kontakte databasen.

Her kan vi bruge dependency injection og give servicen en test-double for mapperen.

25. Backup og restore

Hvis gruppen arbejder med en lokal PostgreSQL-database, kan databasen deles som en backup.

I pgAdmin:

  1. Højreklik på databasen.
  2. Vælg Backup....
  3. Vælg et filnavn.
  4. Brug et passende backup-format.
  5. Sørg for at både struktur og data er med.
  6. Gem backup-filen.
  7. Del filen med gruppen.

For at gendanne:

  1. Opret databasen.
  2. Højreklik på databasen.
  3. Vælg Restore....
  4. Vælg backup-filen.
  5. Gendan databasen.
  6. Refresh databasen i pgAdmin.

26. Hurtigt overblik

Jeg vil…Jeg bruger…
Få en databaseforbindelseconnectionPool.getConnection()
Køre SELECTexecuteQuery()
Køre INSERTexecuteUpdate()
Køre UPDATEexecuteUpdate()
Køre DELETEexecuteUpdate()
Sende en værdi til SQLPreparedStatement
Læse en rækkeResultSet
Læse tekstrs.getString()
Læse et heltalrs.getInt()
Gå til næste rækkers.next()
Mappe database → JavaMapper
Placere forretningslogikService
Kommunikere med HTTPController

Typisk flow

Browser
Controller
Service
Mapper
ConnectionPool
PostgreSQL
ResultSet
Entity / DTO
Service
Controller
Thymeleaf
Browser

Mapperen skal kende SQL.

Servicen skal kende programmets regler.

Controlleren skal kende HTTP.

Databasen skal gemme data.