Общее
Основные принципы и механизмы работы модулей баз данных в Kora.
Мы придерживаемся концепции, что самый лучший способ общения с базой данных SQL, это общение на ее родном языке SQL. Другие инструменты зачастую имеют ограничения на использования специфичных функций определенной базы данных, либо сложный программный язык построения запросов который требует дополнительное и значительное время на изучение и освоение, несет в себе кучу не явностей и потенциальных ошибок со стороны разработчика, а также порой имеет низкую производительность.
Сущность¶
Сущность - представления данных из базы данных в виде класса с полями.
Сущности используемые в качестве возвращаемого значения, должны содержать один публичный конструктор. Это может быть как конструктор по умолчанию, так и конструктор с параметрами. Если Kora найдет конструктор с параметрами, то на его основе будет создаваться объект сущности. В случае же с пустым конструктором поля будут заполняться через сеттеры.
Таблица¶
Можно указывать к какой таблице принадлежит сущность, это понадобится в случае использования макросов при построении запросов.
В случае если таблица не указана, макросы будут использовать имя класса в snake_lower_case
Идентификатор¶
Так как все манипуляции с данными происходят посредствам преобразования сущности в запрос к драйверу, то нет надобности выделять специально первичный ключ в рамках сущности для работы с сущностью.
Обозначать что именно является первичным ключом, может пригодиться в рамках использования макросов,
для этого можно использовать аннотацию @Id
.
Последовательный¶
Рассмотрим создание идентификатора как последовательность чисел на примере Postgres, Kora предлагает использовать механизм базы данных identity column.
Пример таблицы для такой сущности будет выглядеть так:
CREATE TABLE IF NOT EXISTS entities
(
id BIGINT GENERATED ALWAYS AS IDENTITY,
name VARCHAR NOT NULL,
PRIMARY KEY (id)
);
Создаваться идентификатор будет на этапе вставки в базу данных, а получать его в коде приложения подразумевается с помощью конструкции возвращаемого значения идентификатора для JDBC или R2DBC при вставке либо использовать специальные конструкции вашей базы данных:
public record Entity(Long id, String name) {}
@Repository
public interface EntityRepository extends JdbcRepository {
@Query("SELECT id, name FROM entities WHERE id = :id")
@Nullable
Entity findById(long id);
@Query("INSERT INTO entities(name) VALUES (:entity.name) RETURNING id")
long insert(Entity entity);
}
data class Entity(val id: Long, val name: String)
@Repository
interface EntityRepository : JdbcRepository {
@Query("SELECT id, name FROM entities WHERE id = :id")
fun findById(id: Long): Entity?
@Query("INSERT INTO entities(name) VALUES (:entity.name) RETURNING id")
fun insert(entity: Entity): Long
}
Случайный¶
Для создания случайного идентификатора предлагается использовать стандартный UUID
из Java:
Пример таблицы для такой сущности будет выглядеть так:
Создаваться идентификатор будет на этапе создания объекта в пользовательском коде приложения:
public record Entity(UUID id, String name) {}
@Repository
public interface EntityRepository extends JdbcRepository {
@Query("SELECT id, name FROM entities WHERE id = :id")
@Nullable
Entity findById(UUID id);
@Query("INSERT INTO entities(id, name) VALUES (:entity.id, :entity.name)")
void insert(Entity entity);
}
Композитный¶
В случае если требуется использовать композитный ключ,
предполагается использовать аннотацию @Embedded
для создания вложенных полей.
Именование¶
По умолчанию имена полей сущностей переводятся в snake_lower_case при извлечении результата.
Если требуется настроить сопоставление конкретных полей из базы данных с сущностью, то можно использовать аннотацию @Column
:
Стратегия именования¶
Если требуется использовать стратегию именования для всей сущности, то предлагается создать реализацию NameConverter
и затем использовать ее в аннотации @NamingStrategy
.
Требуется чтобы реализация NameConverter
имела конструктор без параметров.
Либо использовать доступные стратегии из Kora:
NoopNameConverter
- стратегия использует имя поля по умолчанию.SnakeCaseNameConverter
- стратегия использует snake_lower_case.SnakeCaseUpperNameConverter
- стратегия использует SNAKE_UPPER_CASE.PascalCaseNameConverter
- стратегия использует PascalCase.CamelCaseNameConverter
- стратегия использует camelCase.
Обязательные поля¶
По умолчанию все поля объявленные в сущности считаются обязательными (NotNull).
По умолчанию все поля объявленные в сущности которые не используют Kotlin Nullability синтаксис считаются обязательными (NotNull).
Необязательные поля¶
В случае если поле в сущности является необязательным, то есть может отсутствовать то,
можно использовать аннотацию @Nullable
для соответствия поля в Json и DTO.
- Подойдет любая аннотация
@Nullable
, такие какjavax.annotation.Nullable
/jakarta.annotation.Nullable
/org.jetbrains.annotations.Nullable
/ и т.д.
Также можно указывать необязательными параметры конструктора в случае если переопределен канонический конструктор у Record:
public record Entity(String id,
String name) {
public Entity(String id,
@Nullable String name) { //(1)!
this.id = id;
this.name = name;
}
}
- Подойдет любая аннотация
@Nullable
, такие какjavax.annotation.Nullable
/jakarta.annotation.Nullable
/org.jetbrains.annotations.Nullable
/ и т.д.
Предполагается использовать Kotlin Nullability синтаксис и помечать такой параметр как Nullable:
Вложенные поля¶
В случае если требуется использовать вложенные поля,
то есть объединить колонки из таблицы в рамках отдельного класса внутри сущности, можно использовать аннотацию @Embedded
.
Предположим есть SQL таблица где имеется композитный ключ который мы хотим выразить отдельным классом:
CREATE TABLE IF NOT EXISTS entities
(
name VARCHAR NOT NULL,
surname VARCHAR NOT NULL,
info VARCHAR NOT NULL,
PRIMARY KEY (name, surname)
)
Тогда сущность будет выглядеть так:
Тогда репозиторий для такой сущности будет выглядеть так:
@Repository
public interface EntityRepository extends JdbcRepository {
@Query("""
SELECT name, surname, info FROM entities
WHERE name = :id.name AND surname = :id.surname;
""")
@Nullable
Entity findById(Entity.UserID id);
@Query("""
INSERT INTO entities(name, surname, info)
VALUES (:entity.id.name, :entity.id.surname, :entity.info)
""")
void insert(Entity entity);
}
@Repository
interface EntityRepository : JdbcRepository {
@Query(
"""
SELECT name, surname, info FROM entities
WHERE name = :id.name AND surname = :id.surname;
"""
)
fun findById(id: Entity.CompositeID): Entity?
@Query(
"""
INSERT INTO entities(name, surname, info)
VALUES (:entity.id.name, :entity.id.surname, :entity.info)
"""
)
fun insert(entity: Entity)
}
В случае если бы поля имели общий префикс, его можно было бы указать в аннотации @Embedded("user_")
:
CREATE TABLE IF NOT EXISTS entities
(
user_name VARCHAR NOT NULL,
user_surname VARCHAR NOT NULL,
info VARCHAR NOT NULL,
PRIMARY KEY (user_name, user_surname)
)
Репозиторий¶
Главным инструментом для работы с базами данных в Kora является использование шаблона проектирование репозиторий при проектировании абстракции доступа к базе данных.
Интерфейс репозитория должен быть проаннотирован @Repository
.
Запросы для методов репозиториев описываются с помощью @Query
аннотации.
На этапе компиляции создается реализация, где каждый метод будет выполнять описанный запрос и эффективно производить сборку аргументов запроса и обработку результата.
Предполагается написать SQL запросов разработчиком, поскольку это повышает ответственность разработчика за план запроса, дает больше понимание и контекста разработчику о том что он делает и как его запрос будет работать. Для улучшения пользовательского опыта с перечислениями моделей можно использовать макросы.
Репозиторий должен являться наследником одной из реализаций, в примерах ниже рассматривается реализация JDBC
@Repository //(1)!
public interface EntityRepository extends JdbcRepository {
public record Entity(String id, String name) { }
@Query("SELECT id, name FROM entities WHERE id = :id") //(2)!
@Nullable //(3)!
Entity findById(String id);
}
- Указывает, что интерфейс является репозиторием.
- Указывает, что нужно создать реализацию метода, выполняющую SQL запрос указанный в аннотации.
- Указывает, что возвращаемое значение может отстутствовать, можно также использовать
Optional
@Repository //(1)!
interface EntityRepository : JdbcRepository {
data class Entity(val id: String, val name: String)
@Query("SELECT id, name FROM entities WHERE id = :id") //(2)!
fun findById(id: String): Entity?
}
- Указывает, что интерфейс является репозиторием.
- Указывает, что нужно создать реализацию метода, выполняющую SQL запрос указанный в аннотации.
Пакетный запрос¶
Kora поддерживает пакетные запросы (batch-запросы) с помощью аннотации @Batch
.
В отличие от последовательного выполнения SQL запросов, пакетная обработка даёт возможность отправить целый набор запросов за один вызов, уменьшая количество требуемых сетевых подключений и позволяя выполнять какое-то количество запросов параллельно на стороне базы данных, что может увеличить скорость выполнения.
Пример использования:
@Repository
public interface EntityRepository extends JdbcRepository {
@Query("INSERT INTO entities(id, name) VALUES (:entity.id, :entity.name)")
void insert(@Batch List<Entity> entity);
}
Пакетный запрос не может возвращать произвольные значения, такой метод может возвращать void
, либо UpdateCount
,
либо созданные базой данных индетификаторы для JDBC или R2DBC драйверов.
@Repository
interface EntityRepository : JdbcRepository {
@Query("INSERT INTO entities(id, name) VALUES (:entity.id, :entity.name)")
fun insert(@Batch entity: List<Entity>)
}
Пакетный запрос не может возвращать произвольные значения, такой метод может возвращать Unit
, либо UpdateCount
,
либо созданные базой данных индетификаторы для JDBC или R2DBC драйверов.
Счетчик обновлений¶
Kora не обрабатывает содержимое запроса, результат метода всегда считается производным из строк, которые вернула база данных.
Если необходимо получить в качестве результата количество обновленных строк нужно использовать специальный тип UpdateCount
.
Ручное управление¶
В случае если не хватает функционала по каким то причинам с запросами в @Query
аннотации или требуется ручное управление соединением,
можно использовать встроенный метод фабрики соединений для создания метода с полностью ручным управлением.
Можно также использовать внутри метода другие методы репозитория и они также будут выполняться в рамках одной транзакции если это требуется. Детальнее про транзакции стоит смотреть документацию по конкретной реализации репозитория.
@Repository
public interface EntityRepository extends JdbcRepository {
public record Entity(Long id, String name) {}
default int insert(Entity entity) {
return getJdbcConnectionFactory().inTx(connection -> {
String sql = "INSERT INTO entities(name) VALUES (?) RETURNING id";
try(PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setString(1, entity.name());
try(ResultSet resultSet = preparedStatement.executeQuery()) {
return resultSet.getInt(1);
}
}
});
}
}
@Repository
interface EntityRepository : JdbcRepository {
data class Entity(val id: Long, val name: String)
fun insert(entity: Entity): Int {
return jdbcConnectionFactory.inTx<Int> { connection ->
val sql = "INSERT INTO entities(name) VALUES (?) RETURNING id"
connection.prepareStatement(sql).use { preparedStatement ->
preparedStatement.setString(1, entity.name)
preparedStatement.executeQuery().use { resultSet -> resultSet.getInt(1) }
}
}
}
}
Макросы¶
Самой неприятной частью написания SQL запросов может быть перечисление и поддержание в соответствие колонок и полей сущности в актуальном состоянии.
Чтобы решить эту проблему можно использовать специальные макрос конструкции внутри SQL запроса в рамках @Query
аннотации.
Эти конструкции позволяют оперировать сущностью на которую указывают и раскрывать её в определенные SQL конструкции и легко дополнять SQL запросы.
Макрос является помощником при написании SQL запросов, раскрывается в конструкции которые пользователь смог бы написать собственными руками.
Синтаксис макроса выглядит следующем образом: %{return#selects}
- Макрос ограничен синтаксической конструкцией
%{
и}
- Первым указывается цель макроса, это может быть как имя любого аргумента метода, так и возвращаемое значение с помощью ключевого слова
return
- Затем используется
#
символ для разделения цели и команды макроса - Затем указывается команда макроса, которая говорит в какую именно SQL конструкцию раскрывать сущность
@Repository
public interface EntityRepository extends JdbcRepository {
@Table("entities")
public record Entity(@Id Long id,
@Column("entity_name") String name,
String code) {}
@Query("SELECT %{return#selects} FROM %{return#table}") //(1)!
List<Entity> findAll();
}
- Раскрывается в запрос:
Команды¶
Доступные команды макросов:
table
- конструкция раскрывает значение сущности в аннотации@Table
либо если таковая отсутствует то, переводит имя сущности в snake_lower_caseselects
- создает конструкцию перечисления колонок сущности дляSELECT
запросаinserts
- создает конструкцию таблицы, перечисления колонок и соответствующих полей сущности дляINSERT
запросаupdates
- создает конструкцию перечисления колонок и соответствующих полей сущности (за исключением@id
поля) дляUPDATE
запросаwhere
- создает конструкцию перечисления колонок со значением из сущности дляWHERE
части запроса
Перечисление полей¶
Макрос поддерживает дополнительный синтаксис по перечислению определенных полей в команде,
если вдруг требуется сделать частичное обновление или получение данных.
Для этого после команды используется специальная конструкция: %{return#updates=name}
Только между полями перечисления и символом перечисления могут содержаться пробелы.
Доступны специальные символы перечисления:
=
- только указанные после символа имя полей сущности будут участвовать в раскрытии команды-=
- все поля сущности за исключением указанных после символа будут участвовать в раскрытии команды
@Repository
public interface EntityRepository extends JdbcRepository {
@Table("entities")
public record Entity(@Id Long id,
@Column("entity_name") String name,
String code) {}
@Query("INSERT INTO %{entity#inserts=name,code}") //(1)!
UpdateCount insert(Entity entity);
}
- Раскрывается в запрос:
@Repository
interface EntityRepository : JdbcRepository {
@Table("entities")
data class Entity(@field:Id val id: Long,
@field:Column("entity_name") val name: String,
val code: String)
@Query("INSERT INTO %{entity#inserts=name,code}") //(1)!
fun insert(entity: Entity): UpdateCount
}
- Раскрывается в запрос:
Идентификатор¶
При перечислении полей в макросе возможно использовать специальное ключевое слово @id
для того чтобы ссылаться сразу идентификатор сущности проаннотированный аннотацией @Id
.
Это может быть особенно полезно когда идентификатор является составным ключом, для перечисления сразу всех колонок.
@Repository
public interface EntityRepository extends JdbcRepository {
@Table("entities")
public record Entity(@Id Long id,
@Column("entity_name") String name,
String code) {}
@Query("INSERT INTO %{entity#inserts-=@id}") //(1)!
UpdateCount insert(Entity entity);
}
- Раскрывается в запрос:
@Repository
interface EntityRepository : JdbcRepository {
@Table("entities")
data class Entity(@field:Id val id: Long,
@field:Column("entity_name") val name: String,
val code: String)
@Query("INSERT INTO %{entity#inserts-=@id}") //(1)!
fun insert(entity: Entity): UpdateCount
}
- Раскрывается в запрос:
Пример репозитория¶
Пример полного репозитория со всеми основными методами для оперирования сущностью для Postgres SQL:
@Repository
public interface EntityRepository extends JdbcRepository {
@Table("entities")
record Entity(@Id String id,
@Column("value1") int field1,
String value2,
@Nullable String value3) {}
@Query("SELECT %{return#selects} FROM %{return#table} WHERE id = :id") //(1)!
@Nullable
Entity findById(String id);
@Query("SELECT %{return#selects} FROM %{return#table}") //(2)!
List<Entity> findAll();
@Query("INSERT INTO %{entity#inserts}") //(3)!
UpdateCount insert(@Batch List<Entity> entity);
@Query("UPDATE %{entity#table} SET %{entity#updates} WHERE %{entity#where = @id}") //(4)!
UpdateCount update(@Batch List<Entity> entity);
@Query("INSERT INTO %{entity#inserts} ON CONFLICT (id) DO UPDATE SET %{entity#updates}") //(5)!
UpdateCount upsert(@Batch List<Entity> entity);
@Query("DELETE FROM entities WHERE id = :id")
UpdateCount deleteById(String id);
@Query("DELETE FROM entities")
UpdateCount deleteAll();
}
- Раскрывается в запрос:
- Раскрывается в запрос:
- Раскрывается в запрос:
- Раскрывается в запрос:
- Раскрывается в запрос:
@Repository
interface EntityRepository : JdbcRepository {
@Table("entities")
data class Entity(
@field:Id val id: String,
@field:Column("value1") val field1: Int,
val value2: String,
@field:Nullable val value3: String
)
@Query("SELECT %{return#selects} FROM %{return#table} WHERE id = :id") //(1)!
fun findById(id: String): Entity?
@Query("SELECT %{return#selects} FROM %{return#table}") //(2)!
fun findAll(): List<Entity>
@Query("INSERT INTO %{entity#inserts}") //(3)!
fun insert(@Batch entity: List<Entity>): UpdateCount
@Query("UPDATE %{entity#table} SET %{entity#updates} WHERE %{entity#where = @id}") //(4)!
fun update(@Batch entity: List<Entity>): UpdateCount
@Query("INSERT INTO %{entity#inserts} ON CONFLICT (id) DO UPDATE SET %{entity#updates}") //(5)!
fun upsert(@Batch entity: List<Entity>): UpdateCount
@Query("DELETE FROM entities WHERE id = :id")
fun deleteById(id: String): UpdateCount
@Query("DELETE FROM entities")
fun deleteAll(): UpdateCount
}
- Раскрывается в запрос:
- Раскрывается в запрос:
- Раскрывается в запрос:
- Раскрывается в запрос:
- Раскрывается в запрос:
Пример композитного¶
Пример репозитория с композитным идентификатором и основными методами для оперирования сущностью,
он почти что полностью идентичен предыдущему за исключением WHERE
условий при поиске и удалении для Postgres SQL:
@Repository
public interface EntityRepository extends JdbcRepository {
@Table("entities")
record Entity(@Id @Embedded EntityId id,
@Column("value1") int field1,
String value2,
@Nullable String value3) {
public record EntityId(String code, String type) { }
}
@Query("SELECT %{return#selects} FROM %{return#table} WHERE %{id#where}") //(1)!
@Nullable
Entity findById(EntityId id);
@Query("SELECT %{return#selects} FROM %{return#table}") //(2)!
List<Entity> findAll();
@Query("INSERT INTO %{entity#inserts}") //(3)!
UpdateCount insert(@Batch List<Entity> entity);
@Query("UPDATE %{entity#table} SET %{entity#updates} WHERE %{entity#where = @id}") //(4)!
UpdateCount update(@Batch List<Entity> entity);
@Query("INSERT INTO %{entity#inserts} ON CONFLICT (code, type) DO UPDATE SET %{entity#updates}") //(5)!
UpdateCount upsert(@Batch List<Entity> entity);
@Query("DELETE FROM entities WHERE %{id#where}")
UpdateCount deleteById(EntityId id);
@Query("DELETE FROM entities")
UpdateCount deleteAll();
}
- Раскрывается в запрос:
- Раскрывается в запрос:
- Раскрывается в запрос:
- Раскрывается в запрос:
- Раскрывается в запрос:
@Repository
interface EntityRepository : JdbcRepository {
@Table("entities")
data class Entity(
@field:Id @field:Embedded val id: EntityId,
@field:Column("value1") val field1: Int,
val value2: String,
val value3: String?
) {
data class EntityId(val code: String, val type: String)
}
@Query("SELECT %{return#selects} FROM %{return#table} WHERE %{id#where}") //(1)!
fun findById(id: EntityId): Entity?
@Query("SELECT %{return#selects} FROM %{return#table}") //(2)!
fun findAll(): List<Entity>
@Query("INSERT INTO %{entity#inserts}") //(3)!
fun insert(@Batch entity: List<Entity>): UpdateCount
@Query("UPDATE %{entity#table} SET %{entity#updates} WHERE %{entity#where = @id}") //(4)!
fun update(@Batch entity: List<Entity>): UpdateCount
@Query("INSERT INTO %{entity#inserts} ON CONFLICT (code, type) DO UPDATE SET %{entity#updates}") //(5)!
fun upsert(@Batch entity: List<Entity>): UpdateCount
@Query("DELETE FROM entities WHERE %{id#where}")
fun deleteById(id: EntityId): UpdateCount
@Query("DELETE FROM entities")
fun deleteAll(): UpdateCount
}
- Раскрывается в запрос:
- Раскрывается в запрос:
- Раскрывается в запрос:
- Раскрывается в запрос:
- Раскрывается в запрос:
Пример наследования¶
Также можно создать абстрактный общий репозиторий и потом использовать его в наследовании для Postgres SQL:
public interface PostgresJdbcCrudRepository<K, V> extends JdbcRepository {
@Query("SELECT %{return#selects} FROM %{return#table}")
List<V> findAll();
@Query("INSERT INTO %{entity#inserts}")
UpdateCount insert(V entity);
@Query("INSERT INTO %{entity#inserts}")
UpdateCount insert(@Batch List<V> entity);
@Query("UPDATE %{entity#table} SET %{entity#updates} WHERE %{entity#where = @id}")
UpdateCount update(V entity);
@Query("UPDATE %{entity#table} SET %{entity#updates} WHERE %{entity#where = @id}")
UpdateCount update(@Batch List<V> entity);
@Query("INSERT INTO %{entity#inserts} ON CONFLICT (%{entity#selects = @id}) DO UPDATE SET %{entity#updates}")
UpdateCount upsert(V entity);
@Query("INSERT INTO %{entity#inserts} ON CONFLICT (%{entity#selects = @id}) DO UPDATE SET %{entity#updates}")
UpdateCount upsert(@Batch List<V> entity);
@Query("DELETE FROM %{entity#table} WHERE %{entity#where = @id}")
UpdateCount delete(V entity);
@Query("DELETE FROM %{entity#table} WHERE %{entity#where = @id}")
UpdateCount delete(@Batch List<V> entity);
}
@Repository
public interface EntityRepository extends PostgresJdbcCrudRepository<String, Entity> {
@Table("entities")
record Entity(@Id String id,
@Column("value1") int field1,
String value2,
@Nullable String value3) {
}
@Query("DELETE FROM entities WHERE id = :id")
UpdateCount deleteById(String id);
@Query("DELETE FROM entities")
UpdateCount deleteAll();
}
interface PostgresJdbcCrudRepository<K, V> : JdbcRepository {
@Query("SELECT %{return#selects} FROM %{return#table}")
fun findAll(): List<V>
@Query("INSERT INTO %{entity#inserts}")
fun insert(entity: V): UpdateCount
@Query("INSERT INTO %{entity#inserts}")
fun insert(@Batch entity: List<V>): UpdateCount
@Query("UPDATE %{entity#table} SET %{entity#updates} WHERE %{entity#where = @id}")
fun update(entity: V): UpdateCount
@Query("UPDATE %{entity#table} SET %{entity#updates} WHERE %{entity#where = @id}")
fun update(@Batch entity: List<V>): UpdateCount
@Query("INSERT INTO %{entity#inserts} ON CONFLICT (%{entity#selects = @id}) DO UPDATE SET %{entity#updates}")
fun upsert(entity: V): UpdateCount
@Query("INSERT INTO %{entity#inserts} ON CONFLICT (%{entity#selects = @id}) DO UPDATE SET %{entity#updates}")
fun upsert(@Batch entity: List<V>): UpdateCount
@Query("DELETE FROM %{entity#table} WHERE %{entity#where = @id}")
fun delete(entity: V): UpdateCount
@Query("DELETE FROM %{entity#table} WHERE %{entity#where = @id}")
fun delete(@Batch entity: List<V>): UpdateCount
}
@Repository
interface EntityRepository : PostgresJdbcCrudRepository<String, Entity> {
@Table("entities")
data class Entity(
@field:Id val id: String,
@field:Column("value1") val field1: Int,
val value2: String,
@field:Nullable val value3: String
)
@Query("DELETE FROM entities WHERE id = :id")
fun deleteById(id: String): UpdateCount
@Query("DELETE FROM entities")
fun deleteAll(): UpdateCount
}