en | ru

mJDBCv1.3.1

компактная, простая и удобная библиотека для JDBC

О библиотеке

  • Компактность - около 50кб, нет дополнительных зависимостей.
  • Простота - не нужно учить лишнее. Инициализация: от 1 строки кода.
  • Надежность - SQL запросы проверяются на момент старта приложения.
  • Гибкость - доступ к низкоуровневому функционалу JDBC.
  • Производительность - нет дополнительных вычислительных расходов по сравнению с JDBC.
  • Транзакционность - методы могут выполнять роль транзакций.
  • Оптимизированность - SQL соединение открывается в момент исполнения первого запроса.
  • Расширяемость - регистрация новых типов данных, переопределение существующих.
  • Профилировка - доступ метрикам исполнения запросов и транзакций.
  • Открытость - открытый исходный код: возможность изменять и исправлять.
  • Надежность - используется в реальных проектах.

Установка

Добавьте следующий код в проектный файл Maven:
<dependency>
    <groupId>com.github.mjdbc</groupId>
    <artifactId>mjdbc</artifactId>
    <version>1.3.1</version>
</dependency>

Примеры использования

Пример 1: SQL запросы
// Шаг 1: Создаем mJDBC адаптер для java.sql.DataSource
java.sql.DataSource ds = ...;
Db db = DbFactory.wrap(ds);

// Шаг 2: Описываем SQL интерфейс
public interface UserSql {
    @Sql("INSERT INTO users(login) VALUES (:login)")
    int insertUser(@BindBean User user);

    @Sql("SELECT * FROM users WHERE login = :login")
    User getUserByLogin(@Bind("login") String login)
}

// Шаг 3: Определяем правила считывания структур из ResultSet
db.registerMapper(User.class, r -> {
    User user = new User();
    user.id = r.getInt("id");
    user.login = r.getString("login");
    ...
    return user;
};)

// Шаг 4: Получаем реализацию SQL интерфейса
UserSql userSql = db.attachSql(UserSql.class);

// Шаг 5: Используем SQL интерфейс для исполнения запросов
User user = userSql.getUserByLogin("login");
Пример 2: Транзакции
// Шаг 1: Создаем интерфейс с транзакционными методами
public interface UserDbi {
    void registerUser(User user);
}

// Шаг 2: Имплементируем интерфейс
public class UserDbiImpl implements UserDbi {
    public void registerUser(User user) {
        User copy = userSql.getUserByLogin(user.login);
        assertNull(copy, "Пользователь уже существует: " + user.login);
        user.id = userSql.insertUser(user);
    }
}

// Шаг 3: Получаем транзакционный прокси-адаптер для интерфейса
UserDbi userDbi = db.attachDbi(UserDbi.class, new UserDbiImpl());

// Шаг 4: Используем адаптер для вызова методов имплементации
userDbi.registerUser(new User("root"));

Обзор mJDBC адаптера

Для того, чтобы начать использовать mJDBC необходимо создать адаптер типа com.github.mjdbc.Db поверх экземпляра javax.sql.DataSource. Для этого нужно использовать статический метод-фабрику:

static DbFactory wrap(DataSource ds)

Транзакционные интерфейсы позволяют выполнить серию SQL-запросов как единую транзакцию давая гарантию, что либо все SQL операции внутри транзакции будут завершены успешено, либо ни одна из них не будет иметь эффект при завершении транзакции. Для регистрации транзакционных интерфейсов используйте следующий метод:

<T> T attachDbi(T impl, Class<T> dbiInterface)

SQL интерфейсы - выразительный способ описывать SQL запросы проверяемые на корректность на момент регистрации интерфейса. Метод регистрации интерфейса возвращает Proxy-класс с реализацией SQL методов изначально описанных в качестве @Sql аннотаций для методов интерфейса.

<T> T attachSql(Class<T> sqlInterface)

Типы параметров для SQL интерфейсов могут быть расширены при помощи регистрации реализаций интерфейса DbBinder<T>, который обеспечивает отображение значения параметра в JDBC параметр для java.sql.PreparedStatement

<T> void registerBinder(Class<? extends T> binderClass, DbBinder<T> binder)
За возвращаемые значения SQL запросов отвечают зарегистрированные экземпляры класса DbMapper<T>. mJDBC поддерживает примитивные Java типы, JDBC типы и списки. Поддержка новых типов может быть расширена за счет использования следующего метода:
<T> void registerMapper(Class<T> mapperClass, DbMapper<T> mapper)

Помимо создания транзакционных и SQL интерфейсов mJDBC позволяет создавать прямые запросы к базе. При этом вызванный метод будет обернут в транзакцию так, если бы использовался транзацкионный интерфейс.

Внутри у метода будет доступ как к управляемому JDBC соединению (DbConnection), так и к DbPreparedStatement классу который поддерживает работу с DbBinder/DbMapper и именованными параметрами. Помимо этого будет доступ к реальным JDBC объектам.

Все вызовы mJDBC аннотированы с @Nullable/@NotNull аннотациями для жесткого контроля работы с нулевыми значениями. Более того, операции которые могут и не могут возвращать нулевые значения разделены.

@Nullable
<T> T execute(@NotNull DbOp<T> op)

@NotNull
<T> T executeNN(@NotNull DbOpNN<T> op)

void executeV(@NotNull  DbOpV op)
                

Доступ к простейшей профилировочной информации о числе вызовов и суммарном времени исполнения транзационных методов и SQL-запросов можно получить используя метод getTimers:

Map<Method, DbTimer> getTimers()

Транзакции

mJDBC предоставляет следующий механизм поддержки транзакций:
  • Разработчик описывает и имплементирует Java интерфейс.
  • mJDBC предоставляет свою прокси-имплементацию этого интерфейса, при вызове методов которого создаются транзакции и вызывается оригинальная имплементация.
  • Разработчик работает с proxy-имплементацией своего интерфейса.

Каждый метод интерфейса представляет из себя отдельную транзакцию: метод commit для транзакции вызывается при успешном завершении метода. Метод rollback - если имплементация производит исключение.

В случае, если один транзакционный метод вызывает другой транзакционный метод, новой транзакции не создается. Вызванный метод становится частью верхнеуровневой транзакции.

Реализация транзакций mJDBC устроена так, что реальная транзакция, так же как и запрос соединения у java.sql.DataSource происходит не в момент вызова прокси-метода, а при первом реальном исполнении SQL. Таким образом, вызовы которые не производят операций с базой, например, используют кэш или проверяют аргументы и возвращают код ошибки, выполняются значительно быстрее.

Транзакционные интерфейсы ничем не отличаются от обычных Java интерфейсов: mJDBC не требует наследования маркерных интерфейсов или наличия аннотаций. Для получения имплементации транзакционного интерфейса необходимо зарегистрировать его и его имплементацию при помощи вызова:

@NotNull
<T> T attachDbi(@NotNull T impl, @NotNull Class<T> dbiInterface)

Сокращение dbi здесь означает: database interface.

Пример использования транзакционного dbi интерфейса: SampleDbi, SampleDbiImpl, SamplesTest.

SQL интерфейс

SQL интерфейс - это Java интерфейс, где каждый метод является отдельным SQL запросом. mJDBC создает готовую к использованию имплементацию для таких интерфейсов. Разработчик использует созданную mJDBC имплементацию для исполнения операций с базой данных вызывая методы интерфейса.

Для того, чтобы описать SQL интерфейс нужно:

  • Создать обычный Java интерфейс, где каждый метод будет соответствовать одному SQL запросу.
  • Написать для каждого метода реальный SQL запрос на языке используемой базы данных и поместить этот запрос в виде аннотации @Sql к методу.
  • Привязать параметры Java метода к параметрам SQL запроса используя @Bind и @BindBean аннотации.
  • Получить имплементацию описанного интерфейса у mJDBC.
Пример SQL интерфейса:
public interface UserSql {
    @NotNull
    @GetGeneratedKeys
    @Sql("INSERT INTO users(login, first_name, last_name)
                    VALUES (:login, :firstName, :lastName)")
    UserId insertUser(@BindBean User user);

    @Nullable
    @Sql("SELECT * FROM users WHERE id = :id")
    User getUserById(@Bind("id") UserId id);

    @Nullable
    @Sql("SELECT * FROM users WHERE login = :login")
    User getUserByLogin(@Bind("login") String login);

    @Sql("SELECT COUNT(*) FROM users")
    int countUsers();
}

Для того, чтобы получить реализацию этого интерфейса следует вызвать следующий метод mJDBC:

@NotNull
<T> T attachSql(@NotNull Class <T> sqlInterface);

При этом произойдет парсинг и проверка запроса на предмет корректности подстановки параметров и возвращаемых значений.

Подставляемые параметры в SQL запросе именованы и имеют префикс к имени в виде двоеточия: ':'.

Соответствие SQL и Java параметров строится по их именам. Для этого для Java параметров используется аннотация @Bind в которой указывается имя соответствующего SQL параметра. Также, для объектных типов возможно использование аннотации @BindBean: в этом случае в качестве параметров SQL будут подставлены имена и значения полей Java объекта. Поля объекта должны быть публичными, либо иметь публичные get-методы.

Исполнение каждого SQL запроса приведет к созданию и исполнению java.sql.PreparedStatement. При этом, для параметров имеющих базовые Java или JDBC типы вызовется соответствующий метод из PreparedStatement. Для того, чтобы в качестве параметров использовать свои собственные типы данных необходимо зарегистрировать в mJDBC механизм отображения параметров в виде имплементации класса DbBinder, либо унаследовать интерфейс описывающий поддерживаемый mJDBC тип: DbInt, DbString etc.

В случае, если SQL запрос возвращает нестандартный тип данных необходимо зарегистрировать его отображение из java.sql.ResultSet при помощи реализации класса DbMapper. Списки (java.util.List) и базовые Java и JDBC типы поддерживаются mJDBC автоматически.

Для возврата авто-генерируемого значения, можно использовать аннотацию @GetGeneratedKeys. Использование @GetGeneratedKeys в коде не обязательно для запросов начинающихся с префикса "INSERT ": для них возврат генерируемого значения происходит по умолчанию.

В случае, если Java параметр является коллекцией (java.util.Iterable), итератором (java.util.Iterator) или Java массивом, вместо одного запроса будет выполнена серия batch-запросов.

SQL параметры

Для работы с параметрами SQL интерфейса используются интерфейс DbBinder и аннотации @Bind и @BindBean. Интерфейс DbBinder описывает как параметр указанного Java типа должен быть сериализован в качестве параметра java.sql.PreparedStatements:

public interface DbBinder<T> {
    void bind(PreparedStatement statement, int idx, T value)
}

При исполнении SQL-запроса mJDBC автоматически подбирает подходящую реализацию DbBinder из набора всех зарегистрированных. При этом, приоритет поиска DbBinder следующий:

  • Точное совпадение с типом класса параметра.
  • Точное совпадение с типом родительского класса параметра.
  • Совпадение с одним из имплементированных интерфейсов.

При наличии более одного подходящего интерфейса производится исключение: java.lang.IllegalArgumentException. Процесс подбора DbBinder происходит на момент регистрации SQL интерфейса в mJDBC.

Для регистрации новых реализаций DbBinder используется метод

<T> void registerBinder(Class<? extends T> binderClass, DbBinder<T> binder)

Аннотация @Bind используется для указания точного имени параметра, как указано в SQL запросе.

Аннотация @BindBean говорит о том, что имена параметров должны совпадать с именами публичных полей или get-методов объекта. В текущей версии mJDBC параметр описанный при помощи @BindBean должен быть единственным параметром интерфейса.

В случае, если SQL запрос содержит больше именованных параметров, чем доступно из данных @Bind и @BindBean аннотаций, производится исключение. Лишние параметры со стороны Java игнорируются. Имена параметров могут содержать только символы Java идентификаторов.

Проверка на полноту и корректность SQL параметров происходит в момент регистрации SQL-интерфейса.

SQL результаты

mJDBC необходимо знать каким образом преобразовывать возвращаемые SQL интерфейсом данные в Java объекты. По умолчанию mJDBC поддерживает все примитивные Java типы и их объектные аналоги, типы java.sql.Date, java.sql.Timestamp, java.util.Date, java.math.BigDecimal и интерфейс списков java.util.List. Для списков в качестве реализации используется java.util.ArrayList.

Для того, чтобы расширить диапазон поддерживаемых типов необходимо зарегистрировать реализацию интерфейса DbMapper, которая из текущей позиции ResultSet создаст и заполнит нужными значениями экземпляр возвращаемого объекта:

public interface DbMapper<T> {
    T map(@NotNull java.sql.ResultSet r) throws SQLException;
}

Например:

DbMapper<User> mapper = (r) -> {
    User user = new User();
    user.id = new UserId(r.getInt("id"));
    user.login = r.getString("login");
    return user;
}
db.registerMapper(User.class, mapper);

Существует несколько техник, которые позволяют mJDBC автоматически находить необходимые реализации DbMapper для возвращаемых методами SQL интерфесов типов, делая их ручную регистрацию опциональной. Для этого, для возвращаемого типа T должно быть выполнено следующее условие:

  • Присутствует публичное статическое финальное поле с аннотацией @Mapper и типом DbMapper<T>

Автоматический поиск нужного экземпляра происходит при условии, что для типа T нет экземпляра DbMapper<T> зарегистрированного вручную.

Поиск происходит на момент регистрации SQL интерфейса. В случае, если в результате поиска найдено 2 или более кандидата на роль DbMapper, выбрасывается исключение и регистрации SQL-интерфейса не происходит.

Повторные прямые вызовы метода registerMapper допускаются и приводят к регистрации нового обработчика DbMapper для указанного типа.

Batch операции

Batch операции используются для оптимизированного исполнения серии однотипных операций над базой данных. Например:

@Sql("UPDATE users SET score = :score WHERE id = :id")
void updateScore(@Bind("id") int[] ids, @Bind("score") int score);

@Sql("UPDATE users SET score = :score WHERE id = :id")
void updateScore(@BindBean List<User> users);

@Sql("INSERT INTO users(login, score) VALUES (:login, :score)")
void insertUsers(@BindBean List<User> users);

При этом mJDBC использует один экземпляр java.sql.PreparedStatement с помощью которого исполняются стандартные JDBC addBatch/executeBatch операции.

Количество вызовов метода addBatch перед каждым executeBatch задается параметром batchChunkSize аннотации @Sql. Значение по умолчанию для этого параметра: Integer.MAX_VALUE.

@Sql(value = "UPDATE users SET score = :score WHERE id = :id",
     batchChunkSize = 100)
void updateScore(@BindBean List<User> users);

                

Batch операции поддерживаются для параметров SQL-интерфейса описанных с использованием аннотаций @Bind и @BindBean. При этом допускается присутствие только одного batch-параметра в запросе.

Batch-параметр определяется по принадлежности к одному из следующих типов: Java массив, java.util.Iterable или java.util.Iterator.

Профилировка

mJDBC отслеживает суммарное время исполнения и количество вызовов для всех зарегистрированных SQL-запросов и транзакционных методов. Для доступа к этой информации используется метод
@NotNull
Map<Method, DbTimer> getTimers();
где DbTimer является следующей структурой:
public final class DbTimer {
    @NotNull
    private final Method method;

    protected volatile long invocationCount;

    protected volatile long totalTimeInNanos;

Низкоуровневый интерфейс

Транзакции

mJDBC позволяет работать с транзакциями и SQL запросами без использования транзакционных и SQL интерфейсов, что может быть удобно для небольших приложений, либо если текущий функционал интерфейсов mJDBC недостаточен.

Для того, чтобы выполнить транзакционную SQL операцию без использования Транзакционного интерфейса можно использовать одни из следующих методов mJDBC:

@Nullable
<T> T execute(@NotNull DbOp<T> op);

@NotNull
<T> T executeNN(@NotNull DbOpNN<T> op);

void executeV(@NotNull DbOpV op);

При вызове любого из этих методов имплементация интерфейса DbOp будет исполнена как отдельная транзакция, также как исполняется отдельный метод транзакционного интерфейса.

/**
 * Database operation with @Nullable result.
 */
public interface DbOp<T> {
    @Nullable
    T run(@NotNull DbConnection c) throws Exception;
}

/**
 * Database operation with no result.
 */
public interface DbOpV {
    void run(@NotNull DbConnection c) throws Exception;
}

/**
 * Database operation with @NotNull result.
 */
public interface DbOpNN<T> {
    @NotNull
    T run(@NotNull DbConnection c) throws Exception;
}

Присутствие трех различных интерфейсов отличающихся лишь типом возвращаемого результата сделано для удобства использования и возможности использования статических анализаторов кода.

Параметр DbConnection предоставляет прямой доступ к java.sql.Connection и используется mJDBC для использования совместно с классом DbPreparedStatement описанным ниже.

В случае, если верхнеуровневый DbOp.run метод завершается без выбрасывания исключения, у текущей транзакции вызывается метод commit. Иначе вызывается метод rollback. Вложенные вызовы execute(DbOp) не приводят к созданию новых транзакций и встраиваются в транзакционный контекст вызывающего метода.

SQL запросы

Для создания SQL запросов без использования SQL интерфейса mJDBC предоставляет класс DbPreparedStatement. Этот класс является оболочкой и предоставляет полный доступ к java.sql.PreparedStatement, а также предоставляет следующий дополнительный функционал:

  • Работа с именованными параметрами.
  • Проверка корректности привязки параметров на момент создания.
  • Отображения результатов в Java объекты при помощи DbMapper.
  • Автоматическое освобождение ресурсов при завершении транзакции/соединения.

Примеры использования DbPreparedStatement совместно с DbOp:

List<User> users = db.execute(c ->
    new DbPreparedStatement<>(c, "SELECT * FROM users", User.class)
        .queryList());
User user = db.execute(c ->
    new DbPreparedStatement<>(c, "SELECT * FROM users WHERE login = :login", User.class).set("login", "admin").query());

Все операции с DbPreparedStatement выполненные внутри DbOp метода являются частью одной транзакции. Все связанные с ними ресурсы отслеживаются mJDBC и автоматически высвобождаются по завершении транзакции.

Рекомендации

  • Используйте примеры с открытым исходным кодом: тесты, другие проекты на GitHub.
  • Используйте пулы для соединения с базой данных. Пример: HikariCP