Kora фреймворк для написания Java / Kotlin приложений с упором на производительность, эффективность, прозрачность сделанный разработчиками Т-Банк / Тинькофф

Kora is a framework for writing Java / Kotlin applications with a focus on performance, efficiency, transparency made by T-Bank / Tinkoff developers

Перейти к содержанию

Общее

Основные принципы и механизмы работы модулей баз данных в Kora.

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

Сущность

Сущность - представления данных из базы данных в виде класса с полями.

Сущности используемые в качестве возвращаемого значения, должны содержать один публичный конструктор. Это может быть как конструктор по умолчанию, так и конструктор с параметрами. Если Kora найдет конструктор с параметрами, то на его основе будет создаваться объект сущности. В случае же с пустым конструктором поля будут заполняться через сеттеры.

public record Entity(String id, String name) {}
data class Entity(val id: String, val name: String)

Таблица

Можно указывать к какой таблице принадлежит сущность, это понадобится в случае использования макросов при построении запросов.

В случае если таблица не указана, макросы будут использовать имя класса в snake_lower_case

@Table("entities")
public record Entity(String id, String name) {}
@Table("entities")
data class Entity(val id: String, val name: String)

Идентификатор

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

Обозначать что именно является первичным ключом, может пригодиться в рамках использования макросов, для этого можно использовать аннотацию @Id.

public record Entity(@Id String id, String name) {}
data class Entity(@field:Id val id: String, val name: String)

Последовательный

Рассмотрим создание идентификатора как последовательность чисел на примере 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:

Пример таблицы для такой сущности будет выглядеть так:

CREATE TABLE IF NOT EXISTS entities
(
    id   UUID    NOT NULL,
    name VARCHAR NOT NULL,
    PRIMARY KEY (id)
);

Создаваться идентификатор будет на этапе создания объекта в пользовательском коде приложения:

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);
}
data class Entity(val id: UUID, val name: String)

@Repository
interface EntityRepository : JdbcRepository {

    @Query("SELECT id, name FROM entities WHERE id = :id")
    fun findById(id: UUID): Entity?

    @Query("INSERT INTO entities(id, name) VALUES (:entity.id, :entity.name)")
    fun insert(entity: Entity)
}

Композитный

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

Именование

По умолчанию имена полей сущностей переводятся в snake_lower_case при извлечении результата.

Если требуется настроить сопоставление конкретных полей из базы данных с сущностью, то можно использовать аннотацию @Column:

public record Entity(@Column("ID") String id, 
                     @Column("NAME") String name) {}
data class Entity(@field:Column("ID") val id: String,
                  @field:Column("NAME") val name: String)

Стратегия именования

Если требуется использовать стратегию именования для всей сущности, то предлагается создать реализацию NameConverter и затем использовать ее в аннотации @NamingStrategy. Требуется чтобы реализация NameConverter имела конструктор без параметров.

Либо использовать доступные стратегии из Kora:

  • NoopNameConverter - стратегия использует имя поля по умолчанию.
  • SnakeCaseNameConverter - стратегия использует snake_lower_case.
  • SnakeCaseUpperNameConverter - стратегия использует SNAKE_UPPER_CASE.
  • PascalCaseNameConverter - стратегия использует PascalCase.
  • CamelCaseNameConverter - стратегия использует camelCase.
@NamingStrategy(NoopNameConverter.class)
public record Entity(String id, 
                     String name) {}
@NamingStrategy(NoopNameConverter::class.java)
data class Entity(val id: String,
                  val name: String)

Обязательные поля

По умолчанию все поля объявленные в сущности считаются обязательными (NotNull).

public record Entity(String id,
                     String name) {}

По умолчанию все поля объявленные в сущности которые не используют Kotlin Nullability синтаксис считаются обязательными (NotNull).

data class Entity(val id: String,
                  val name: String)

Необязательные поля

В случае если поле в сущности является необязательным, то есть может отсутствовать то, можно использовать аннотацию @Nullable для соответствия поля в Json и DTO.

public record Entity(String id, 
                     @Nullable String name) {} //(1)!
  1. Подойдет любая аннотация @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;
    }
}
  1. Подойдет любая аннотация @Nullable, такие как javax.annotation.Nullable / jakarta.annotation.Nullable / org.jetbrains.annotations.Nullable / и т.д.

Предполагается использовать Kotlin Nullability синтаксис и помечать такой параметр как Nullable:

data class Entity(val id: String,
                  val name: String?)

Вложенные поля

В случае если требуется использовать вложенные поля, то есть объединить колонки из таблицы в рамках отдельного класса внутри сущности, можно использовать аннотацию @Embedded.

Предположим есть SQL таблица где имеется композитный ключ который мы хотим выразить отдельным классом:

CREATE TABLE IF NOT EXISTS entities
(
    name     VARCHAR NOT NULL,
    surname  VARCHAR NOT NULL,
    info     VARCHAR NOT NULL,
    PRIMARY KEY (name, surname)
)

Тогда сущность будет выглядеть так:

public record Entity(@Id @Embedded UserID id,
                     @Column("info") String info) {

    public record UserID(String name, String surname) {}
}
data class Entity(
    @field:Id @field:Embedded val id: UserID,
    @field:Column("name") val info: String
) {

    data class UserID(
        val name: String,
        val surname: String
    )
}

Тогда репозиторий для такой сущности будет выглядеть так:

@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);
}
  1. Указывает, что интерфейс является репозиторием.
  2. Указывает, что нужно создать реализацию метода, выполняющую SQL запрос указанный в аннотации.
  3. Указывает, что возвращаемое значение может отстутствовать, можно также использовать 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?
}
  1. Указывает, что интерфейс является репозиторием.
  2. Указывает, что нужно создать реализацию метода, выполняющую 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.

@Repository
public interface EntityRepository extends JdbcRepository {

    @Query("INSERT INTO entities(id, name) VALUES (:entity.id, :entity.name)")
    UpdateCount insert(Entity entity);
}
@Repository
interface EntityRepository : JdbcRepository {

    @Query("INSERT INTO entities(id, name) VALUES (:entity.id, :entity.name)")
    fun insert(entity: Entity): 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}

  1. Макрос ограничен синтаксической конструкцией %{ и }
  2. Первым указывается цель макроса, это может быть как имя любого аргумента метода, так и возвращаемое значение с помощью ключевого слова return
  3. Затем используется # символ для разделения цели и команды макроса
  4. Затем указывается команда макроса, которая говорит в какую именно 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();
}
  1. Раскрывается в запрос:
    SELECT id, entity_name, code FROM entities
    
@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("SELECT %{return#selects} FROM %{return#table}") //(1)!
    fun findAll(): List<Entity>
}
  1. Раскрывается в запрос:
    SELECT id, entity_name, code FROM entities
    

Команды

Доступные команды макросов:

  • table - конструкция раскрывает значение сущности в аннотации @Table либо если таковая отсутствует то, переводит имя сущности в snake_lower_case
  • selects - создает конструкцию перечисления колонок сущности для SELECT запроса
  • inserts - создает конструкцию таблицы, перечисления колонок и соответствующих полей сущности для INSERT запроса
  • updates - создает конструкцию перечисления колонок и соответствующих полей сущности (за исключением @id поля) для UPDATE запроса
  • where - создает конструкцию перечисления колонок со значением из сущности для WHERE части запроса

Перечисление полей

Макрос поддерживает дополнительный синтаксис по перечислению определенных полей в команде, если вдруг требуется сделать частичное обновление или получение данных. Для этого после команды используется специальная конструкция: %{return#updates=name}

Только между полями перечисления и символом перечисления могут содержаться пробелы.

Доступны специальные символы перечисления:

  1. = - только указанные после символа имя полей сущности будут участвовать в раскрытии команды
  2. -= - все поля сущности за исключением указанных после символа будут участвовать в раскрытии команды
@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);
}
  1. Раскрывается в запрос:
    INSERT INTO entities(entity_name, code) 
    VALUES(:entity.name, :entity.code)
    
@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
}
  1. Раскрывается в запрос:
    INSERT INTO entities(entity_name, code) 
    VALUES(:entity.name, :entity.code)
    
Идентификатор

При перечислении полей в макросе возможно использовать специальное ключевое слово @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);
}
  1. Раскрывается в запрос:
    INSERT INTO entities(entity_name, code) 
    VALUES(:entity.name, :entity.code)
    
@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
}
  1. Раскрывается в запрос:
    INSERT INTO entities(entity_name, code) 
    VALUES(:entity.name, :entity.code)
    

Пример репозитория

Пример полного репозитория со всеми основными методами для оперирования сущностью для 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();
}
  1. Раскрывается в запрос:
    SELECT id, value1, value2, value3 
    FROM entities 
    WHERE id = :id
    
  2. Раскрывается в запрос:
    SELECT id, value1, value2, value3 
    FROM entities
    
  3. Раскрывается в запрос:
    INSERT INTO entities(id, value1, value2, value3) 
    VALUES(:entity.id, :entity.value1, :entity.value2, :entity.value3)
    
  4. Раскрывается в запрос:
    UPDATE entities
    SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 
    WHERE id = :entity.id
    
  5. Раскрывается в запрос:
    INSERT INTO entities(id, value1, value2, value3) 
    VALUES(:entity.id, :entity.value1, :entity.value2, :entity.value3)
    ON CONFLICT (id) DO UPDATE 
    SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 
    
@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
}
  1. Раскрывается в запрос:
    SELECT id, value1, value2, value3 
    FROM entities 
    WHERE id = :id
    
  2. Раскрывается в запрос:
    SELECT id, value1, value2, value3 
    FROM entities
    
  3. Раскрывается в запрос:
    INSERT INTO entities(id, value1, value2, value3) 
    VALUES(:entity.id, :entity.value1, :entity.value2, :entity.value3)
    
  4. Раскрывается в запрос:
    UPDATE entities
    SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 
    WHERE id = :entity.id
    
  5. Раскрывается в запрос:
    INSERT INTO entities(id, value1, value2, value3) 
    VALUES(:entity.id, :entity.value1, :entity.value2, :entity.value3)
    ON CONFLICT (id) DO UPDATE 
    SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 
    
Пример композитного

Пример репозитория с композитным идентификатором и основными методами для оперирования сущностью, он почти что полностью идентичен предыдущему за исключением 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();
}
  1. Раскрывается в запрос:
    SELECT code, type, value1, value2, value3 
    FROM entities 
    WHERE code = :code AND type = :type
    
  2. Раскрывается в запрос:
    SELECT code, type, value1, value2, value3 
    FROM entities
    
  3. Раскрывается в запрос:
    INSERT INTO entities(code, type, value1, value2, value3) 
    VALUES(:entity.code, :entity.type, :entity.value1, :entity.value2, :entity.value3)
    
  4. Раскрывается в запрос:
    UPDATE entities
    SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 
    WHERE code = :entity.id.code AND type = :entity.id.type
    
  5. Раскрывается в запрос:
    INSERT INTO entities(code, type, value1, value2, value3) 
    VALUES(:entity.code, :entity.type, :entity.value1, :entity.value2, :entity.value3)
    ON CONFLICT (code, type) DO UPDATE 
    SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 
    
@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
}
  1. Раскрывается в запрос:
    SELECT code, type, value1, value2, value3 
    FROM entities 
    WHERE code = :code AND type = :type
    
  2. Раскрывается в запрос:
    SELECT code, type, value1, value2, value3 
    FROM entities
    
  3. Раскрывается в запрос:
    INSERT INTO entities(code, type, value1, value2, value3) 
    VALUES(:entity.code, :entity.type, :entity.value1, :entity.value2, :entity.value3)
    
  4. Раскрывается в запрос:
    UPDATE entities
    SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 
    WHERE code = :entity.id.code AND type = :entity.id.type
    
  5. Раскрывается в запрос:
    INSERT INTO entities(code, type, value1, value2, value3) 
    VALUES(:entity.code, :entity.type, :entity.value1, :entity.value2, :entity.value3)
    ON CONFLICT (code, type) DO UPDATE 
    SET value1 = :entity.field1, value2 = :entity.value2, value3 = :entity.value3 
    
Пример наследования

Также можно создать абстрактный общий репозиторий и потом использовать его в наследовании для 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
}