Подключение к базе данных в Java: используем JDBC. На примере H2 database

На прошлой неделе я был занят тестовым заданием с весьма специфичными требованиями: в программе запрещалось использовать фреймворки, поэтому подключение к базе данных пришлось делать по старинке. Я старинку уже забыл, так как в последний раз бился с базами данных в таком формате ещё в универе. Сейчас оказалось, что подключения с JDBC очень просто создавать, но я всё равно завис над этой задачей на несколько часов из-за пары странных исключений. Про них я тоже расскажу в этой короткой статье.

Добавление зависимости в pom.xml

Для тестового задания я выбрал базу H2, так как она очень проста в использовании. Не нужно ничего устанавливать, достаточно просто прописать в pom-файле зависимость:

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>2.1.210</version>
        </dependency>

Версию можете выбрать другую, если вам не понравится эта.

Конфигурация базы данных

Так как задание тестовое и база у меня не самая «модная», то и воспользоваться можно самыми стандартными настройками для in-memory базы (она хранится в памяти и уничтожается после завершения работы приложения):

    private static final String jdbcURL = "jdbc:h2:mem:testdb";
    private static final String jdbcUser = "sa";
    private static final String jdbcPass = "";

Можно также заменить первую строку на вариант из документации – тогда база окажется в домашнем каталоге (странно это выражение по-русски звучит… имеется в виду home dir):

private static final String jdbcURL = "jdbc:h2:~/test";

Если вы работаете с другой базой, система всё та же. Те же три значения: где искать базу и как в неё попасть. Например, для Оракла значения будут подобные:

    private static final String jdbcURL = "jdbc:oracle:thin:@//[HOST][:PORT]/SERVICE";
    private static final String username = "dbUser";
    private static final String password = "password";

Но статья всё же не об Оракле – не зря ж я решил упростить всё и выбрал себе H2 🙂 Поэтому дальше о нём ни слова.

Работа с классом DBConnector

За соединение с базой в моём приложении отвечает класс DBConnector. За выдачу соединений всем желающим – метод getConnection().

package com.demo.data.db;

//список импортов опущен

@ApplicationScoped
public class DBConnector {

    private static final String driver = "org.h2.Driver";
    private static final String jdbcURL = "jdbc:h2:~/test";
    private static final String jdbcUser = "sa";
    private static final String jdbcPass = "";

    @SneakyThrows
    public Connection getConnection() throws SQLException {
        Class.forName(driver);
        Connection connection = DriverManager.getConnection(jdbcURL, jdbcUser, jdbcPass);
        connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
        return connection;
    }

}

Строчка Class.forName(«org.h2.Driver») отвечает за регистрацию драйвера. Изначально я её пропустил, а потом мучился с исключениями, которые, к счастью, быстро поправил.

DriverManager.getConnection добывает для нас подключение на основе адреса, логина и пароля.

Ну и потом можно делать с подключением, что только захочется. Я вот позаботился о безопасности данных с помощью метода setTransactionIsolation перед возвращением подключения.

Исключения, конечно, лучше бы обработать сразу, но я не стал загромождать код. Блок try-catch поможет вам.

Исключение java.sql.SQLException.

Как я уже отметил выше, я упустил строчку с регистрацией драйвера, поэтому получил исключение java.sql.SQLException: No suitable driver found for jdbc:h2:~/test.

Добавление Class.forName(«org.h2.Driver»); решило эту проблему.

Из-за того, что подключение не было установлено и база в итоге была пустой, я также получил такое сообщение:

Servlet.service() for servlet com.demo.MyApplication threw exception

java.lang.NullPointerException

В этом случае не стоит искать никакой null и перелопачивать логику, а просто стоит удостовериться, что с драйверов и подключением всё в порядке. \

Исключение javax.servlet.ServletException: org.glassfish.jersey.server.ContainerException: java.lang.NoClassDefFoundError: javax/xml/parsers/ParserConfigurationException

Это исключение потрепало мне нервы. Теперь решение кажется вполне очевидным, но тогда у меня не две минуты ушло на исправление ситуации. Я использовал Glassfish 4.1.2, который скачал весной, потому что тогда Spring капризничал, и мне посоветовали с ним работать на старой версии.

А на этот раз в интернете советовали вообще отказаться от рыбного сервера. Люди на стэковерфлоу спорили, какая из версий наименее глючная и предлагали то томкат, то wildfly, то неизвестные мне варианты. Я даже хотел их попробовать и уже настраивал профили для них, но потом оказалось, что достаточно всего лишь вернуться на более современную версию. Переход на Glassfish 5.0.1 мне помог.

На всякий случай покажу тут страшное сообщение об ошибке (целиком, чтобы вы тоже попугались).

Servlet.service() for servlet com.clangame.demo.MyApplication threw exception
java.lang.ClassNotFoundException: javax.xml.parsers.ParserConfigurationException not found by org.eclipse.persistence.moxy [228]
	at org.apache.felix.framework.BundleWiringImpl.findClassOrResourceByDelegation(BundleWiringImpl.java:1532)
	at org.apache.felix.framework.BundleWiringImpl.access$400(BundleWiringImpl.java:75)
	at org.apache.felix.framework.BundleWiringImpl$BundleClassLoader.loadClass(BundleWiringImpl.java:1955)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	at org.eclipse.persistence.jaxb.BeanValidationHelper.<clinit>(BeanValidationHelper.java:53)
	at org.eclipse.persistence.jaxb.JAXBBeanValidator.isConstrainedObject(JAXBBeanValidator.java:257)
	at org.eclipse.persistence.jaxb.JAXBBeanValidator.shouldValidate(JAXBBeanValidator.java:208)
	at org.eclipse.persistence.jaxb.JAXBMarshaller.validateAndTransformIfNeeded(JAXBMarshaller.java:587)
	at org.eclipse.persistence.jaxb.JAXBMarshaller.marshal(JAXBMarshaller.java:481)
	at org.eclipse.persistence.jaxb.rs.MOXyJsonProvider.writeTo(MOXyJsonProvider.java:949)
	at org.glassfish.jersey.message.internal.WriterInterceptorExecutor$TerminalWriterInterceptor.invokeWriteTo(WriterInterceptorExecutor.java:265)
	at org.glassfish.jersey.message.internal.WriterInterceptorExecutor$TerminalWriterInterceptor.aroundWriteTo(WriterInterceptorExecutor.java:250)
	at org.glassfish.jersey.message.internal.WriterInterceptorExecutor.proceed(WriterInterceptorExecutor.java:162)
	at org.glassfish.jersey.server.internal.JsonWithPaddingInterceptor.aroundWriteTo(JsonWithPaddingInterceptor.java:106)
	at org.glassfish.jersey.message.internal.WriterInterceptorExecutor.proceed(WriterInterceptorExecutor.java:162)
	at org.glassfish.jersey.server.internal.MappableExceptionWrapperInterceptor.aroundWriteTo(MappableExceptionWrapperInterceptor.java:86)
	at org.glassfish.jersey.message.internal.WriterInterceptorExecutor.proceed(WriterInterceptorExecutor.java:162)
	at org.glassfish.jersey.message.internal.MessageBodyFactory.writeTo(MessageBodyFactory.java:1130)
	at org.glassfish.jersey.server.ServerRuntime$Responder.writeResponse(ServerRuntime.java:683)
	at org.glassfish.jersey.server.ServerRuntime$Responder.processResponse(ServerRuntime.java:424)
	at org.glassfish.jersey.server.ServerRuntime$Responder.process(ServerRuntime.java:414)
	at org.glassfish.jersey.server.ServerRuntime$2.run(ServerRuntime.java:312)
	at org.glassfish.jersey.internal.Errors$1.call(Errors.java:271)
	at org.glassfish.jersey.internal.Errors$1.call(Errors.java:267)
	at org.glassfish.jersey.internal.Errors.process(Errors.java:315)
	at org.glassfish.jersey.internal.Errors.process(Errors.java:297)
	at org.glassfish.jersey.internal.Errors.process(Errors.java:267)
	at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:317)
	at org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:292)
	at org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:1139)
	at org.glassfish.jersey.servlet.WebComponent.service(WebComponent.java:460)
	at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:386)
	at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:334)
	at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:221)
	at org.apache.catalina.core.StandardWrapper.service(StandardWrapper.java:1682)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:318)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:160)
	at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:734)
	at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:673)
	at com.sun.enterprise.web.WebPipeline.invoke(WebPipeline.java:99)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:174)
	at org.apache.catalina.connector.CoyoteAdapter.doService(CoyoteAdapter.java:416)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:283)
	at com.sun.enterprise.v3.services.impl.ContainerMapper$HttpHandlerCallable.call(ContainerMapper.java:459)
	at com.sun.enterprise.v3.services.impl.ContainerMapper.service(ContainerMapper.java:167)
	at org.glassfish.grizzly.http.server.HttpHandler.runService(HttpHandler.java:206)
	at org.glassfish.grizzly.http.server.HttpHandler.doHandle(HttpHandler.java:180)
	at org.glassfish.grizzly.http.server.HttpServerFilter.handleRead(HttpServerFilter.java:235)
	at org.glassfish.grizzly.filterchain.ExecutorResolver$9.execute(ExecutorResolver.java:119)
	at org.glassfish.grizzly.filterchain.DefaultFilterChain.executeFilter(DefaultFilterChain.java:283)
	at org.glassfish.grizzly.filterchain.DefaultFilterChain.executeChainPart(DefaultFilterChain.java:200)
	at org.glassfish.grizzly.filterchain.DefaultFilterChain.execute(DefaultFilterChain.java:132)
	at org.glassfish.grizzly.filterchain.DefaultFilterChain.process(DefaultFilterChain.java:111)
	at org.glassfish.grizzly.ProcessorExecutor.execute(ProcessorExecutor.java:77)
	at org.glassfish.grizzly.nio.transport.TCPNIOTransport.fireIOEvent(TCPNIOTransport.java:536)
	at org.glassfish.grizzly.strategies.AbstractIOStrategy.fireIOEvent(AbstractIOStrategy.java:112)
	at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy.run0(WorkerThreadIOStrategy.java:117)
	at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy.access$100(WorkerThreadIOStrategy.java:56)
	at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy$WorkerThreadRunnable.run(WorkerThreadIOStrategy.java:137)
	at org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.doWork(AbstractThreadPool.java:591)
	at org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.run(AbstractThreadPool.java:571)
	at java.lang.Thread.run(Thread.java:748)

Использование подключения

Готовое подключение можно использовать как и в этом же классе (да хоть в том же методе), так и в других. У меня оно передавалось на слой с DAO, которые и писали туда запросы. Для примера покажу пару примеров, но подробнее углублюсь в это в другой раз:

    public void save(Task task) {
        String insertQuery = "INSERT INTO task (description) VALUES (?)";
        try (Connection connection = connector.getConnection();
             PreparedStatement insertPreparedStatement
                     = connection.prepareStatement(insertQuery)) {
            insertPreparedStatement.setString(1, task.getDescription());
            insertPreparedStatement.executeUpdate();
        } catch (SQLException ex) {
            ex.printStackTrace();
        }
    }

Обязательно нужно всё закрывать, иначе произойдёт «утечка» – то есть объекты останутся в подвешенном состоянии. У меня эта услуга включена в try-catch блок с автозакрытием ресурсов.

connection.prepareStatement(insertQuery) используется для запросов с параметрами. Если их нет, то ничего готовить не надо, инъекции быть не может, поэтому защищаться от неё тоже не нужно. Тогда подойдёт и connector.getConnection().

    public List<Task> getAll() {
        String sql = "SELECT * FROM task";
        List<Task> tasks = new ArrayList<>();
        try (Connection connection = connector.getConnection();
             Statement stmt = connection.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {
            while (rs.next()) {
                Task task = new Task();
                task.setId(rs.getLong("task_id"));
                task.setDescription(rs.getString("description"));
                tasks.add(task);
            }
        } catch (SQLException ex) {
            ex.printStackTrace();
        }

        return tasks;
    }

Что ж, получилось не так коротко, как я ожидал. Но зато информативно. С настройкой и подключением разобрались, своей глупостью я поделился (потому что делиться мудростью пока не позволяет опыт) и даже примеры работы с базой с JDBC показал. Может, кто-то тоже олдскульный или выполняет учебные/ тестовые задания и вынужден отказаться от удобных фреймворков.

В следующей статье подробнее разберу это тестовое приложение – Rest-сервис без фрейморков, но с поддержкой многопоточности (которую работодатель забраковал, но я всё равно её сюда выложу – может, кому-то пригодится).

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s