- Подключаться к базе через JDBC, отправлять запросы и читать
ResultSet - Предотвращать SQL-инъекции с помощью
PreparedStatement - Управлять транзакциями и объяснять роль пула соединений и JPA
Когда программа закрывается, все объекты в памяти исчезают. А оценки учеников, заказы и учётные записи пользователей должны храниться годами — их держат в базе данных. В курсе SQL ты писал запросы вручную; теперь сама программа на Java подключится к базе, отправит SQL и превратит результат в объекты. Основа всего этого — 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) |
|---|---|---|
| PostgreSQL | jdbc:postgresql://localhost:5432/school | org.postgresql:postgresql |
| MySQL | jdbc:mysql://localhost:3306/school | com.mysql:mysql-connector-j |
| SQLite | jdbc:sqlite:school.db | org.xerial:sqlite-jdbc |
| H2 (в памяти) | jdbc:h2:mem:school | com.h2database:h2 |
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
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();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) отключает этот режим, и транзакцией можно управлять вручную.
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 использует его по умолчанию.
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
}com.zaxxer:HikariCP); не пиши пароль в коде — читай его из переменной окруженияJPA и Hibernate: объекты без SQL
В JDBC каждый столбец приходится вручную переносить в объект. ORM (Object-Relational Mapping) автоматизирует это: класс сопоставляется таблице, поле — столбцу, объект — строке. Стандарт ORM в Java — JPA (Jakarta Persistence, пакет jakarta.persistence), а самая популярная его реализация — Hibernate. Класс помечают @Entity, @Id обозначает первичный ключ, а @Column задаёт имя столбца и ограничения.
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; }
}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.
executeUpdate?