JDBC API в Java - обзор и туториал
jdbc
java database
2016-01-24 Туториалы по программированию
На этот раз я хотел бы уделить внимание работе с базами данных с помощью JDBC API.
Спор под названием Hibernate (точнее JPA) против JDBC оставим в стороне. Оба подхода имеют право на существование. Если хотите услышать мое личное мнение по этому поводу, то загляните в конец статьи.
А сейчас давайте рассмотрим следующие вопросы:
- DriverManager и JDBC драйвер
- Соединение к базе данных
- Использование Statement и ResultSet
- PreparedStatement и пакетное выполнение запросов
- Транзакции в JDBC
- Использование DatabaseMetaData
- Заключение
DriverManager и JDBC Driver
Во всех примерах подключения к базе данных в Интернете вы обязательно встретите эти строки:
Class.forName(driverClass);
Connection connection = DriverManager
.getConnection(url, user, password) ;
Где driverClass - это строка с полным именем класса JDBC драйвера, например org.h2.Driver для H2 Database или com.mysql.jdbc.Driver для MySql.
Обычно на этом повествование про драйвер и DriverManager заканчивается. Ну а мы копнем чуть глубже.
Все основные сущности в JDBC API, с которыми вам предстоит работать, являются интерфейсами:
- Connection;
- Statement;
- PreparedStatement;
- CallableStatement;
- ResultSet;
- Driver;
- DatabaseMetaData.
JDBC драйвер конкретной базы данных как раз и предоставляет реализации этих интерфейсов.
DriverManager - это синглтон, который содержит информацию о всех зарегистрированных драйверах. Метод getConnection на основании параметра URL находит java.sql.Driver соответствующей базы данных и вызывает у него метод connect.
Так а зачем же вызов Class.forName()?
Если посмотреть исходный код реализации любого драйвера он будет содержать статический блок инициализации такого вида:
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException e) {
throw new RuntimeException("Can't register driver!");
}
}
Вызов Class.forName загружает класс и этим гарантирует выполнение статического блока инициализации, а значит и регистрацию драйвера в DriverManager.
Соединение к базе данных
В статье я предлагаю воспользоваться легковесной базой данных, написанной на Java, под названием H2 Database. В данный момент для нас преимущество ее использования состоит в том, что скачав джарку здесь вы получаете сразу и саму базу данных, и драйвер для подключения к ней, что для обучения очень удобно.
Подробнее об H2 я обязательно расскажу в отдельной статье, а пока что создадим соединение к базе данных:
Class.forName("org.h2.Driver");
Connection connection = DriverManager.getConnection("jdbc:h2:mem:test");
Таким образом, мы получили реализацию интерфейса java.sql.Connection для нашей базы данных.
Полный код всех примеров можно найти в конце статьи.
Используем Statement и ResultSet
На основании соединения можно получить объект java.sql.Statement для выполнения запросов к базе.
Statement statement = connection.createStatement();
statement.execute("create table user(" +
"id integer primary key auto_increment, " +
"name varchar(100));");
В результате выполнения этого фрагмента кода будет создана таблица user с двумя колонками id и name.
Statement можно использовать для выполнения любых запросов, будь то DDL, DML, либо обычные запросы на выборку данных.
statement.execute("insert into user(name) values('borya'),('petya')");
ResultSet rs = statement.executeQuery("select * from user");
while (rs.next()) {
System.out.println(rs.getInt("id") + " : " + rs.getString("name"));
}
Объект ResultSet - это результат выполнения запроса.
Объекты Connection, Statement и ResultSet после использования необходимо закрывать. Поэтому приведенный выше код необходимо обернуть в try-finally и в блоке finally добавить закрытие ресурсов:
if (rs != null)
try { rs.close(); }
catch (SQLException ignore) { }
if (statement != null)
try { statement.close(); }
catch (SQLException ignore) { }
if (connection != null)
try { connection.close(); }
catch (SQLException ignore) { }
Выглядит не очень красиво, не правда ли. Закрытие ResultSet можно убрать, ведь в соответствии с контрактом:
A ResultSet object is automatically closed by the Statement object that generated it when that Statement object is closed, re-executed, or is used to retrieve the next result from a sequence of multiple results.
Но все равно не то…
С приходом Java 1.7 ситуация немного изменилась в лучшую сторону, так как была добавлена конструкция try-with-resources, которая гарантирует что все Closeable ресурсы будут закрыты после выполнения try блока. Наш код превращается в следующий более элегантный фрагмент:
try (Connection connection = DriverManager.getConnection("jdbc:h2:mem:test");
Statement statement = connection.createStatement()) {
statement.execute("create table user(" +
"id integer primary key auto_increment, " +
"name varchar(100));");
statement.execute("insert into user(name) values('borya'),('petya')");
ResultSet rs = statement.executeQuery("select * from user");
while (rs.next()) {
System.out.println(rs.getInt("id") + " : " + rs.getString("name"));
}
}
PreparedStatement и пакетное выполнение запросов
Если вам нужно выполнить несколько похожих запросов, то разумным решением будет использование PreparedStatement.
PreparedStatement представляет собой скомпилированную версию SQL-выражения, выполнение которого будет быстрее и эффективнее.
PreparedStatement statement = connection
.prepareStatement("insert into user(id,name) values(?,?)");
statement.setInt(1, 3);
statement.setString(2, "fedya");
statement.executeUpdate();
PreparedStatement поддерживает пакетную (batch) отправку SQL запросов, что значительно уменьшает траффик между клиентом и базой данных. Небольшой пример:
PreparedStatement statement = connection
.prepareStatement("insert into user(id,name) values(?,?)");
statement.setInt(1, 4);
statement.setString(2, "misha");
statement.addBatch();
statement.setInt(1, 5);
statement.setString(2, "grisha");
statement.addBatch();
statement.executeBatch();
Обратите внимание, что проставлять параметры в PreparedStatement необходимо через индексы, к тому же отсчет идет с единицы. Если параметров много и есть вероятность, что они периодически будут добавляться или удаляться, то можно воспользоваться таким вариантом:
PreparedStatement statement = connection
.prepareStatement("insert into user(id,name) values(?,?)");
int i = 0;
statement.setInt(++i, 4);
statement.setString(++i, "misha");
statement.executeUpdate();
Транзакции в JDBC
Тех, кто знакомился с Hibernate минуя JDBC, обычно очень удивляет работа с транзакциями.
По умолчанию каждое SQL-выражение автоматически коммитится при выполнении statement.execute и подобных методов. Для того, чтобы открыть транзакцию сначала необходимо установить флаг autoCommit у соединения в значение false. Ну а дальше нам пригодятся всем знакомые методы commit и rollback.
connection.setAutoCommit(false);
Statement st = connection.createStatement();
try {
st.execute("insert into user(name) values('kesha')");
connection.commit();
} catch (SQLException e) {
connection.rollback();
}
Использование DatabaseMetaData
С помощью Connection можно получиь очень полезную сущность DatabaseMetaData. Она позволяет получить метаинформацию о схеме базы данных, а именно какие в базе данных есть объекты - таблицы, колонки, индексы, триггеры, процедуры и так далее.
Лично я часто использую DatabaseMetaData для модификации схемы базы данных программным способом, например:
rs = connection.getMetaData()
.getTables(c.getCatalog(), null, "USER", null);
if (!rs.next()) {
Statement statement = connection.createStatement()) {
statement.execute("create table user(" +
"id integer primary key auto_increment, " +
"name varchar(100));");
}
После добавления новой функциональности можно проверить, а не созданы ли уже соответствующие объекты, и в случае необходимости модифицировать схему.
Заключение
Конечно, JDBC API создано далеко не идеальным. Например, SQLException является checked исключением и его повсюду надо тянуть или оборачивать; работа с PreparedStatement достаточно неудобная, как мы уже видели.
Но в подавляющем большинстве случаев я использую именно JDBC для свои приложений, так как JDBC дает максимальную гибкость и эффективность. Возможно с Hibernate вы сэкономите один день, так как вам не придется писать код для создания схемы, а так же запросы для чтения и записи объектов. Но что такое один день по сравнению со временем существования приложения? К тому же практика показывает, что периодически Hibernate преподносит разрабочикам интересные челенджи, решение которых может забрать не только время, но и нервы
Полный исходный код примеров из статьи можно найти здесь SqlExamples.java.