Перейти к содержанию
Educora
Продвинутый23 мин16 / 18

JDBC и базы данных

Подключи Java-программу к базе данных через JDBC: `PreparedStatement`, защита от SQL-инъекций, транзакции, пулы соединений и введение в JPA/Hibernate.

Проверь себя
В этом уроке ты узнаешь
  • Подключаться к базе через JDBC, отправлять запросы и читать ResultSet
  • Предотвращать SQL-инъекции с помощью PreparedStatement
  • Управлять транзакциями и объяснять роль пула соединений и JPA

Когда программа закрывается, все объекты в памяти исчезают. А оценки учеников, заказы и учётные записи пользователей должны храниться годами — их держат в базе данных. В курсе SQL ты писал запросы вручную; теперь сама программа на Java подключится к базе, отправит SQL и превратит результат в объекты. Основа всего этого — JDBC.

Определение
JDBC

Java Database Connectivity — стандартный API из пакета java.sql. Код пишется через интерфейсы Connection, PreparedStatement, ResultSet, а реализует их драйвер конкретной базы данных (PostgreSQL, MySQL, SQLite, H2). При смене базы обычно меняются только драйвер и строка подключения.

Подключение и первый запрос

Драйвер добавляют в проект как обычную зависимость (в Maven обычно с областью runtime). DriverManager.getConnection(url, user, password) открывает соединение; URL указывает тип и адрес базы. У Statement три основных метода: execute выполняет любой SQL, executeUpdate возвращает число изменённых строк, а executeQuery — таблицу результата, ResultSet. rs.next() переводит курсор на следующую строку и возвращает false, когда строки закончились. Все ресурсы важно закрывать через try-with-resources.

База данныхПример URLДрайвер (Maven)
PostgreSQLjdbc:postgresql://localhost:5432/schoolorg.postgresql:postgresql
MySQLjdbc:mysql://localhost:3306/schoolcom.mysql:mysql-connector-j
SQLitejdbc:sqlite:school.dborg.xerial:sqlite-jdbc
H2 (в памяти)jdbc:h2:mem:schoolcom.h2database:h2
Java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class Main {
    public static void main(String[] args) throws SQLException {
        String url = "jdbc:h2:mem:school";   // the H2 driver must be on the classpath
        try (Connection conn = DriverManager.getConnection(url, "sa", "");
             Statement st = conn.createStatement()) {
            st.execute("CREATE TABLE students (id INT AUTO_INCREMENT PRIMARY KEY, "
                    + "first_name VARCHAR(50), city VARCHAR(50), score INT)");
            int rows = st.executeUpdate("INSERT INTO students (first_name, city, score) VALUES "
                    + "('Aysel', 'Baku', 95), ('Murad', 'Ganja', 78), ('Leyla', 'Baku', 88)");
            System.out.println("Inserted rows: " + rows);

            try (ResultSet rs = st.executeQuery("SELECT first_name, score FROM students ORDER BY score DESC")) {
                while (rs.next()) {
                    System.out.println(rs.getString("first_name") + " " + rs.getInt("score"));
                }
            }
        }
    }
}
Ожидаемый результат
Inserted rows: 3
Aysel 95
Leyla 88
Murad 78
Пример вывода с драйвером H2 (без драйвера программа падает с ошибкой No suitable driver found)

PreparedStatement и SQL-инъекции

Склеивать значение от пользователя со строкой SQL — одна из самых опасных ошибок. Если кто-то введёт в поле логина ' OR '1'='1, условие всегда окажется истинным, и запрос вернёт всех пользователей — это SQL-инъекция. PreparedStatement заранее готовит SQL с заполнителями ?, а значения отправляет отдельно: база всегда воспринимает их как данные, а не как команды. К тому же подготовленный запрос при повторном использовании может работать быстрее.

Опасно: склейка строк
String sql = "SELECT * FROM users WHERE login = '" + login + "'";
ResultSet rs = st.executeQuery(sql);
// login = ' OR '1'='1   ->   WHERE login = '' OR '1'='1'   ->   every user!
Безопасно: параметры `?`
String sql = "SELECT * FROM users WHERE login = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, login);   // sent separately, always treated as data
ResultSet rs = ps.executeQuery();
Java
static List<String> findByCity(Connection conn, String city, int minScore) throws SQLException {
    String sql = "SELECT first_name FROM students WHERE city = ? AND score >= ? ORDER BY first_name";
    List<String> names = new ArrayList<>();
    try (PreparedStatement ps = conn.prepareStatement(sql)) {
        ps.setString(1, city);        // parameters are numbered from 1
        ps.setInt(2, minScore);
        try (ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                names.add(rs.getString("first_name"));
            }
        }
    }
    return names;
}
// findByCity(conn, "Baku", 80)  ->  [Aysel, Leyla]
Типичный метод чтения: параметры, ResultSet и сбор результата в список

В настоящей программе каждую строку обычно превращают в объект: new Student(rs.getLong("id"), rs.getString("first_name"), rs.getInt("score")). Все методы чтения и записи для одной таблицы собирают в одном классе — его называют DAO (Data Access Object) или репозиторием. Так SQL остаётся в одном месте, а остальная программа работает только с объектами Student и не знает, какая база стоит за ними.

Транзакции

Айсель переводит 50 манатов на счёт Мурада: деньги должны уйти с одного счёта и прийти на другой. Если сервер отключится после первого UPDATE, деньги «потеряются». Транзакция превращает несколько операций в одно неделимое целое: либо сохраняются все (commit), либо ни одна (rollback). В JDBC соединение по умолчанию работает в режиме auto-commit — каждый запрос сохраняется сразу. setAutoCommit(false) отключает этот режим, и транзакцией можно управлять вручную.

Java
static void transfer(Connection conn, int fromId, int toId, int amount) throws SQLException {
    String sql = "UPDATE accounts SET balance = balance + ? WHERE id = ?";
    conn.setAutoCommit(false);                    // start a transaction
    try (PreparedStatement ps = conn.prepareStatement(sql)) {
        ps.setInt(1, -amount);
        ps.setInt(2, fromId);
        ps.executeUpdate();
        ps.setInt(1, amount);
        ps.setInt(2, toId);
        ps.executeUpdate();
        conn.commit();                            // both changes are saved together...
    } catch (SQLException e) {
        conn.rollback();                          // ...or neither of them
        throw e;
    } finally {
        conn.setAutoCommit(true);
    }
}
Оба UPDATE либо выполняются вместе, либо вместе отменяются

Пул соединений

Открыть новое соединение дорого: сеть, проверка пароля, память на стороне базы. Веб-приложение, которое делает это для каждого запроса, падает под нагрузкой. Пул соединений (connection pool) заранее открывает несколько соединений и «одалживает» их: getConnection() берёт готовое соединение, а close() не закрывает его, а возвращает в пул. Самый популярный пул в Java — HikariCP; Spring Boot использует его по умолчанию.

Java
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/school");
config.setUsername("school_app");
config.setPassword(System.getenv("DB_PASSWORD"));
config.setMaximumPoolSize(10);

try (HikariDataSource pool = new HikariDataSource(config)) {    // create once per application
    try (Connection conn = pool.getConnection()) {              // borrowed from the pool
        System.out.println(findByCity(conn, "Baku", 80));
    }                                                           // close() returns it to the pool
}
HikariCP (com.zaxxer:HikariCP); не пиши пароль в коде — читай его из переменной окружения

JPA и Hibernate: объекты без SQL

В JDBC каждый столбец приходится вручную переносить в объект. ORM (Object-Relational Mapping) автоматизирует это: класс сопоставляется таблице, поле — столбцу, объект — строке. Стандарт ORM в Java — JPA (Jakarta Persistence, пакет jakarta.persistence), а самая популярная его реализация — Hibernate. Класс помечают @Entity, @Id обозначает первичный ключ, а @Column задаёт имя столбца и ограничения.

Java
import jakarta.persistence.*;

@Entity
@Table(name = "students")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "first_name", nullable = false)
    private String firstName;
    private String city;
    private int score;

    protected Student() {}                        // JPA needs a no-argument constructor

    public Student(String firstName, String city, int score) {
        this.firstName = firstName;
        this.city = city;
        this.score = score;
    }

    public String getFirstName() { return firstName; }
    public int getScore() { return score; }
}
Таблица, описанная как Java-класс
Java
EntityManager em = emf.createEntityManager();

em.getTransaction().begin();
em.persist(new Student("Nigar", "Sumgait", 91));        // Hibernate generates the INSERT
em.getTransaction().commit();

Student first = em.find(Student.class, 1L);             // SELECT ... WHERE id = 1

List<Student> best = em.createQuery(
        "SELECT s FROM Student s WHERE s.score >= :min ORDER BY s.score DESC", Student.class)
    .setParameter("min", 90)
    .getResultList();
EntityManager сам генерирует SQL; запросы пишут на JPQL с именами классов и полей

В JPQL пишут не таблицу students, а класс Student, не столбец first_name, а поле firstName. В следующем уроке ты увидишь, что Spring Data JPA сокращает и этот код: достаточно написать interface StudentRepository extends JpaRepository<Student, Long>, а методы save, findById, findAll Spring создаст сам. Но под ORM по-прежнему работает JDBC, поэтому знание JDBC помогает разбираться в проблемах.

Главное

  • JDBC: Connection → PreparedStatement → ResultSet; драйвер выбирается по URL, ресурсы закрываются через try-with-resources.
  • Никогда не склеивай ввод пользователя с SQL; используй параметры ?, пронумерованные с 1.
  • executeQuery возвращает ResultSet, executeUpdate — число изменённых строк; rs.next() двигает курсор вперёд.
  • Транзакция: setAutoCommit(false) → commit() или rollback() — всё или ничего.
  • Пул соединений (HikariCP) переиспользует соединения; JPA/Hibernate связывает классы с таблицами через @Entity и @Id.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Что возвращает executeUpdate?