Зарегистрируйтесь, чтобы продолжить обучение

Работа с базой данных в Javalin Java: Веб-технологии

Репозитории, которыми мы пользовались в уроках этого курса, хранят свои данные в обычных переменных. Это было удобно для того, чтобы не отвлекаться на взаимодействие с базой и сфокусироваться на особенностях работы веба.

Теперь мы привыкли к фреймворку и понимаем принципы создания приложений на нем, поэтому можно начинать работу с реальной базой данных.

Чтобы начать хранить и извлекать данные из базы, нам нужно выполнить несколько действий:

  1. Подключить пакеты, необходимые для работы с базой данных
  2. Настроить подключение к базе данных и дать к нему доступ из приложения
  3. Создать начальную структуру базы данных с нужными таблицами
  4. Переписать методы репозиториев так, чтобы они работали с данными через базу

В этом уроке мы проделаем все эти шаги на примере создания части CRUD для сущности Car с полями make (марка) и model (модель).

Устанавливаем зависимости

Для простоты мы будем использовать базу данных H2 с хранением в памяти. Этого достаточно в обучении, но в реальном окружении уже понадобится поставить PostgreSQL или его аналог:

implementation("com.h2database:h2:2.2.220")
implementation("com.zaxxer:HikariCP:5.0.1")

В дополнение к самой базе данных, нам понадобится пакет HikariCP. Он предоставляет пул соединений для работы в конкурентной среде, когда множество клиентов выполняют запросы одновременно. В такой ситуации одного соединения с базой данных будет недостаточно. Ниже мы увидим, как это работает.

Настраиваем подключение

Рассмотрим такой пример:

package org.example.hexlet;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

// Остальные импорты

public class HelloWorld {
    public static Javalin getApp() {
        var hikariConfig = new HikariConfig();
        hikariConfig.setJdbcUrl("jdbc:h2:mem:hexlet_project;DB_CLOSE_DELAY=-1;");

        var dataSource = new HikariDataSource(hikariConfig);
        BaseRepository.dataSource = dataSource;

        var app = Javalin.create(config -> {
            config.bundledPlugins.enableDevLogging();
            config.fileRenderer(new JavalinJte());
        });

        // Остальной код
    }
}

В примере выше мы создаем базу данных H2 с именем hexlet_project и расположением в памяти. После создания базы данных мы получаем переменную dataSource, через которую мы будем работать с базой. Доступ к ней нам будет нужен в репозиториях, потому что запросы должны быть сосредоточенны в них.

Поэтому нам нужно создать общий базовый класс для всех репозиториев со статическим полем для хранения этой переменой. Все остальные репозитории от него наследуются:

package org.example.hexlet.repository;

import com.zaxxer.hikari.HikariDataSource;

public class BaseRepository {
    public static HikariDataSource dataSource;
}

Строим начальную структуру базы данных

В нашем случае база данных создается при старте приложения, поэтому ее инициализацию мы будем делать там же, во время старта. Для этого создадим файл с нужной схемой данных и затем добавим ее в базу данных:

  1. Добавляем файл src/main/resources/schema.sql:

    DROP TABLE IF EXISTS courses;
    
    CREATE TABLE courses (
        id SERIAL PRIMARY KEY,
        name VARCHAR(255) NOT NULL,
        description TEXT
    );
    
    DROP TABLE IF EXISTS cars;
    
    CREATE TABLE cars (
        id SERIAL PRIMARY KEY,
        make VARCHAR(255) NOT NULL,
        model VARCHAR(255) NOT NULL
    );
    
  2. Во время инициализации базы данных загружаем схему в базу:

    public static Javalin getApp() throws Exception {
        var hikariConfig = new HikariConfig();
        hikariConfig.setJdbcUrl("jdbc:h2:mem:project;DB_CLOSE_DELAY=-1;");
    
        var dataSource = new HikariDataSource(hikariConfig);
        // Получаем путь до файла в src/main/resources
        var url = HelloWorld.class.getClassLoader().getResourceAsStream("schema.sql");
        var sql = new BufferedReader(new InputStreamReader(url))
            .lines().collect(Collectors.joining("\n"));
    
        // Получаем соединение, создаем стейтмент и выполняем запрос
        try (var connection = dataSource.getConnection();
                var statement = connection.createStatement()) {
            statement.execute(sql);
        }
        BaseRepository.dataSource = dataSource;
    
        var app = Javalin.create(config -> {
            config.bundledPlugins.enableDevLogging();
        });
    }
    

Создаем репозиторий CarRepository

Перейдем к созданию репозитория:

package org.example.hexlet.repository;

import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.example.hexlet.model.Car;

public class CarRepository extends BaseRepository {
    public static void save(Car car) throws SQLException {
        String sql = "INSERT INTO cars (make, model) VALUES (?, ?)";
        try (var conn = dataSource.getConnection();
                var preparedStatement = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
            preparedStatement.setString(1, car.getMake());
            preparedStatement.setString(2, car.getModel());
            preparedStatement.executeUpdate();
            var generatedKeys = preparedStatement.getGeneratedKeys();
            // Устанавливаем ID в сохраненную сущность
            if (generatedKeys.next()) {
                car.setId(generatedKeys.getLong(1));
            } else {
                throw new SQLException("DB have not returned an id after saving an entity");
            }
        }
    }

    public static Optional<Car> find(Long id) throws SQLException {
        var sql = "SELECT * FROM cars WHERE id = ?";
        try (var conn = dataSource.getConnection();
                var stmt = conn.prepareStatement(sql)) {
            stmt.setLong(1, id);
            var resultSet = stmt.executeQuery();
            if (resultSet.next()) {
                var make = resultSet.getString("make");
                var model = resultSet.getString("model");
                var car = new Car(make, model);
                car.setId(id);
                return Optional.of(car);
            }
            return Optional.empty();
        }
    }

    public static List<Car> getEntities() throws SQLException {
        var sql = "SELECT * FROM cars";
        try (var conn = dataSource.getConnection();
                var stmt = conn.prepareStatement(sql)) {
            var resultSet = stmt.executeQuery();
            var result = new ArrayList<Car>();
            while (resultSet.next()) {
                var id = resultSet.getLong("id");
                var make = resultSet.getString("make");
                var model = resultSet.getString("model");
                var car = new Car(make, model);
                car.setId(id);
                result.add(car);
            }
            return result;
        }
    }
}

Принцип создания всех методов для работы с базой данных одинаковый:

  • Описываем шаблон запроса
  • Получаем соединение
  • Формируем стейтмент
  • Делаем подстановки
  • Выполняем запрос
  • Собираем результат
  • Возвращаем ответ

Рассматриваем примеры операций

Структура контроллеров не меняется, несмотря на все изменения, которые мы сделали. Как видно на примере ниже, правильная организация абстракций и разделение по слоям приводят к тому, что изменение внутренностей не оказывает особого влияния на строение приложения:

package org.example.hexlet.controller;

import java.sql.SQLException;
import static io.javalin.rendering.template.TemplateUtil.model;

import org.example.hexlet.dto.cars.CarPage;
import org.example.hexlet.dto.cars.CarsPage;
import org.example.hexlet.model.Car;
import org.example.hexlet.repository.CarRepository;
import org.example.hexlet.util.NamedRoutes;

import io.javalin.http.Context;
import io.javalin.http.NotFoundResponse;

public class CarController {
    public static void index(Context ctx) throws SQLException {
        var cars = CarRepository.getEntities();
        var page = new CarsPage(cars);
        ctx.render("cars/index.jte", model("page", page));
    }

    public static void show(Context ctx) throws SQLException {
        var id = ctx.pathParamAsClass("id", Long.class).get();
        var car = CarRepository.find(id)
                .orElseThrow(() -> new NotFoundResponse("Car with id = " + id + " not found"));
        var page = new CarPage(car);
        ctx.render("cars/show.jte", model("page", page));
    }

    public static void build(Context ctx) {
        ctx.render("cars/build.jte");
    }

    public static void create(Context ctx) throws SQLException {
        var make = ctx.formParam("make");
        var model = ctx.formParam("model");

        var car = new Car(make, model);
        CarRepository.save(car);
        ctx.redirect(NamedRoutes.carsPath());
    }
}

Самостоятельная работа

  1. Проделайте все шаги из урока на своем компьютере на примере сущности курсов
  2. Перейдите в свое приложение и репозиторий курсов и внесите туда изменения так, чтобы репозитории работали с базой данных
  3. Сделайте то же самое для сущности пользователя
  4. Залейте изменения на GitHub

Дополнительные материалы

  1. База данных H2
  2. Пул соединений HikariCP

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff