1. Пул з'єднань (Connection pool)

Сьогодні ми навчимося працювати з базою даних ще професійніше. І поговоримо ми зараз про пул потоків, або connection pool англійською.

Коннект до бази даних вимагає витрат певного часу. Особливо, якщо база даних віддалена. Якщо під кожен запит робити підключення до бази, то відповідь нашої програми буде неймовірно низькою за швидкістю. Не кажучи вже про ресурси, які воно потребує.

Як рішення для таких проблем було запропоновано зберігати з'єднання з базою в певній множині, яку називають пулом потоків.

Коли ми просимо нове з'єднання, то connection pool його створює, а під час закриття він не закриває його, а зберігає до connection pool. І якщо ми знову запитаємо з'єднання у connection pool, то він дасть нам одне зі старих замість того, щоб створювати нове.

По суті, застосунок працює через спеціальний драйвер, який є обгорткою для звичайного JDBC-драйвера і який має додатковий функціонал роботи з пулом. Цю ситуацію можна уявити такою картинкою:

Connection pool

Налаштування для пулу з'єднань програміст може прописати вручну: кількість активних з'єднань, час очікування тощо. Для особливо екстремальних ситуацій можна написати свій connection pool: клас, який буде мати список з'єднань. У нього буде перевизначена функція close, яка повертатиме з'єднання назад до списку, і буде багато інших плюшок на кшталт таймера відкритого з'єднання. Коли немає конекшена довгий час, з'єднання закривається.

2. Інтерфейс DataSource та ConnectionPoolDataSource

Пул потоків зазвичай працює в парі з об'єктом DataSource. Цей об'єкт можна як розглядати як абстракцію віддаленої бази даних. Саму назву Data Source можна дослівно перекласти як Джерело Даних. Що ніби натякає.

DataSource-об'єкти використовуються для отримання фізичного з'єднання з базою даних і є альтернативою DriverManager. Водночас немає потреби реєструвати JDBC-драйвер. Потрібно лише виставити відповідні параметри для встановлення з'єднання та виконати метод getConnection().

При створенні об'єкта типу DataSource локально (прямо у додатку) параметри з'єднання задаються відповідними методами, передбаченими постачальником JDBC-драйвера. Ці методи не визначені інтерфейсом DataSource, тому що параметри для з'єднання з СУБД різних виробників можуть відрізнятися як за типом, так і за кількістю (наприклад, не для всіх СУБД необхідно вказувати тип драйвера або протокол мережі).

Так, під час роботи з СУБД Oracle для використання відповідних set і get методів необхідно отримати об'єкт типу OracleDataSource, який є екземпляром однойменного класу, що реалізує інтерфейс DataSource. Тому такий спосіб створення об'єктів типу DataSource робить твій код менш переносимим і залежним від конкретної реалізації драйвера. Нижче наведено приклад коду, що ілюструє локальне використання об'єктів типу DataSource.


import java.sql.*;
import javax.sql.*;
import oracle.jdbc.driver.*;
import oracle.jdbc.pool.*;

public class DataSource {
    public static void main(String[] args) {
    	try {
        	OracleDataSource ods = new OracleDataSource();

        	ods.setUser("root");
        	ods.setPassword("secret");
        	ods.setDriverType("thin");
        	ods.setDatabaseName("test");
        	ods.setServerName("localhost");
        	ods.setPortNumber(1521);

        	Connection connection = ods.getConnection();
        	System.out.println("Connection successful!!!");

    	} catch (SQLException se) {
        	se.printStackTrace();
    	}
    	System.out.println("Goodbye!");
	}
}

3.* Інтерфейс JNDI

Повністю можливості об'єктів типу DataSource проявляються у поєднанні з використанням інтерфейсу JNDI. JNDI — це спеціальний сервіс (щось на кшталт каталогу) для великих серверних додатків, який дозволяє одним сервісам встановлювати з'єднання з іншими. Нижче наведено деякі стандартні імена властивостей (параметрів), розроблені компанією Sun:

Ім'я властивості Тип даних Java Призначення
databaseName String Ім'я бази даних
serverName String Ім'я сервера
user String Ім'я користувача (ID)
password String Пароль користувача
portNumber Int Номер порту сервера СУБД

Комбінування інтерфейсів DataSource та JNDI відіграє ключову роль у розробці багаторівневих корпоративних застосунків, заснованих на компонентній технології J2EE.

У разі використання комбінації інтерфейсів DataSource та JNDI у програмному коді необхідно лише зробити запит на об'єкт типу DataSource у служби імен та каталогів. Водночас деталі з'єднання і залежний від конкретної реалізації драйвера програмний код виявляється прихованим від програми в об'єкті, що зберігається в службі імен і каталогів. Таким чином спільне використання об'єктів типу DataSource і JNDI передбачає наявність двох виконуваних незалежно один від одного кроків:

  1. Необхідно зберегти іменований об'єкт типу DataSource у службі імен та каталогів за допомогою методу Context.bind() пакету javax.naming.
  2. Зробити запит до програми на об'єкт типу DataSource у служби імен та каталогів, використовуючи метод Context.lookup(). Після цього можна використовувати його метод DataSource.getConnection() для отримання з'єднання з базою даних.

Нижче наведено приклад спільного використання інтерфейсу JNDI та об'єкта типу OracleDataSource.


// Створюємо ключовий JNDI об'єкт
Hashtable env = new Hashtable();
env.put (Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.fscontext.RefFSContextFactory");
env.put (Context.PROVIDER_URL, "file:JNDI");
Context ctx = new InitialContext(env);

// Отримуємо об'єкт DataSource за його іменем
DataSource ods = (DataSource) ctx.lookup("myDatabase");

// Отримуємо Connection з об'єкта DataSource
Connection connection = ods.getConnection();
System.out.println("Connection successful!!!");

Розбирати роботу JNDI ми не будемо. Це виходить за межі цього курсу. Просто хочу, щоб ти знав, що такий підхід часто-густо використовується у великих програмах. Не губися, якщо побачиш у майбутньому такий код.

Не має значення, як це працює. Тобі достатньо просто знати, що за допомогою JNDI сервісу ти можеш отримати proxy-об'єкт будь-якого сервісу, зареєстрованого в каталозі сервісів. Саме таким чином отримують об'єкт DataSource, і за його допомогою працюють з базою даних.

4. Приклади Connection Pool-ів

Давай краще повернемось до того, з чого почали — до пулів з'єднань (connections pool).

Java-програмісти у своїх програмах дуже часто використовують бібліотеки, які містять у собі різні реалізації connection pool-ів. Серед них є три найпопулярніші:

  • Apache Commons DBCP
  • C3PO
  • HikariCP

Першим створили хороший connection pool програмісти з проекту Apache. Він також використовується всередині вебсервера Tomcat. Приклад роботи з ним:

<

public class DBCPDataSource {

    private static BasicDataSource ds = new BasicDataSource();
    static {
    	ds.setUrl("jdbc:h2:mem:test");
    	ds.setUsername("user");
    	ds.setPassword("password");
    	ds.setMinIdle(5);
    	ds.setMaxIdle(10);
    	ds.setMaxOpenPreparedStatements(100);
    }

    public static Connection getConnection() throws SQLException {
    	return ds.getConnection();
    }

    private DBCPDataSource(){ }
}

Другий дуже популярний пул — це C3PO. Дивні назви, згоден. Назва C3PO — це посилання на ім'я робота c3po із Зоряних Війн. А також CP — це скорочення від Connection Pool.


public class C3p0DataSource {

    private static ComboPooledDataSource cpds = new ComboPooledDataSource();
    static {
    	try {
        	cpds.setDriverClass("org.h2.Driver");
        	cpds.setJdbcUrl("jdbc:h2:mem:test");
        	cpds.setUser("user");
        	cpds.setPassword("password");
    	} catch (PropertyVetoException e) {
            // handle the exception
    	}
    }

    public static Connection getConnection() throws SQLException {
    	return cpds.getConnection();
    }

    private C3p0DataSource(){}
}

З документацією по ньому можна ознайомитися на офіційній сторінці.

І нарешті, найпопулярніший Connection Pool у наш час — це HikariCP:


public class HikariCPDataSource {

    private static HikariConfig config = new HikariConfig();
    private static HikariDataSource ds;

    static {
    	config.setJdbcUrl("jdbc:h2:mem:test");
    	config.setUsername("user");
    	config.setPassword("password");
    	config.addDataSourceProperty("cachePrepStmts", "true");
    	config.addDataSourceProperty("prepStmtCacheSize", "250");
    	config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
    	ds = new HikariDataSource(config);
    }

    public static Connection getConnection() throws SQLException {
    	return ds.getConnection();
    }

    private HikariCPDataSource(){}
}

Ось його офіційна сторінка в GitHub.

І хороша стаття про конфігурування.