Попробуйте, наконец, Celesta
в своём проекте!

Иван Пономарёв

Эпиграф

Everything should be made as simple as possible, but not simpler
— attributed to Albert Einstein
einstein
ivan

Иван Пономарёв

  • Staff Engineer @ Synthesized.io

  • Teaching Java @ МФТИ and Mainor

  • PC Member @ Heisenbug, Hydra, FlowConference

На дворе 2023й год…​

Нам всё ещё нужно приложение на Java/Kotlin, работающее с реляционной базой

Что мы выберем?

Connectivity

JDBC API + DataSource

hikari
commonspool

RDBMS

Mssql

Ora

db2

postgresql

mysql

firebird

H2

sqllite

derby

Мигратор

flyway

liquibase

ORM

spring

jooq

hibernate

mybatis

eclipselink

Тестирование

testcontainers transparent

H2

standards 2x

Meet Celesta

celesta duke
  • Мигратор + Type-Safe code-generated ORM + testing helper

  • Вдохновлен ERP-платформами

  • Ответ на сложность разработки и тестирования систем с базами данных

Что такое Celesta?

celesta duke
  • thumbs up Free open source

  • thumbs up Десятки малых и средних проектов. Стабильна!

  • inlove В документацию вложено много любви: courseorchestra.github.io/celesta

  • thumbs up Tooling:

    • Spring Boot Starter

    • JUnit 5 extension

    • Maven plugin (usable from Gradle)

    • IntelliJ IDEA plugin

Проблемы проекта Celesta

celesta duke
  • shrug Раньше поддерживалась компанией, сейчас почти проект одного человека

  • shrug Никак не выйдет за пределы узкого круга моих знакомых и коллег

  • shrug Мало звёзд на гитхабе (если захотите, это легко исправить)!

Celesta

  • Абстракция над 5 типами баз данных. Код не изменяется при замене базы данных!

Diagram

Comparison testing

comparison celesta

CelestaSQL: "virtual" SQL dialect

celesta sql

Type mapping

types

FAQ: Как же мой супер-производительный запрос, использующий специфичную функциональность конкретной СУБД?

  1. А может обойтись функциональностью CelestaSQL?

  2. Всегда можно просверлить дырку в абстракции (заплатив цену)

Внезапная выгода от database agnostic подхода

То, что работает на H2, будет работать и на

  • PostgreSQL

  • Oracle

  • MSSQL

  • Firebird

Проблемы с традиционными миграторами

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog>
    <changeSet>
        ....
    </changeSet>
    <changeSet>
        ....
    </changeSet>
    <changeSet>
        ....
    </changeSet>
</databaseChangeLog>
  • Какова актуальная схема базы данных? — без дополнительных решений не разберёшься!

Это как если бы…​.

class Employee {
    ...
}

alter class Employee add variable String name;

alter class Employee drop method promote();

alter class add method doSomething() {

}

Не лучше ли

CONVERGE TABLE OrderHeader(
  id VARCHAR(30) NOT NULL,
  date DATETIME DEFAULT GETDATE(),
  customer_id VARCHAR(30),
  customer_name VARCHAR(100),
  CONSTRAINT Pk_OrderHeader PRIMARY KEY (id)
);
  • чтобы скрипт выглядел привычно для SQL редакторов, используем CREATE вместо CONVERGE

Как работает миграция Celesta

  1. Сравнение актуального и желательного

  2. Best effort для приведения актуального к желательному (топологическая сортировка, правильный порядок операций и т.д.)

  3. Если у схемы задана версия через

CREATE SCHEMA foo VERSION '1.0';

то миграция не производится к схеме более старой версии.

FAQ: Но ведь это работает не всегда?

  1. Да, в аспекте исключительно автомиграции дела обстоят несколько хуже

  2. На практике, при адекватном подходе, вы не этого не почувствуете (готов обсудить отдельно)

  3. Всегда можно обойти абстракцию и приспособить Flyway (заплатив цену)

Курсоры

cursors

Как работает курсор: чтение по первичному ключу

CallContext ctx = ...
ItemCursor item = new ItemCursor(ctx);
//Type safe API!
item.get(42);
Diagram
select id, name, default_price from item where id = 42 limit 1;

Как работает курсор: чтение по первичному ключу

CallContext ctx = ...
ItemCursor item = new ItemCursor(ctx);
//Type safe API!
item.get(42);
Diagram
select id, name, default_price from item where id = 42 limit 1;

Как работает курсор: модификация данных

//Type-safe, camelCase API
item.setDefaultPrice(14.9); //<---
item.update();
Diagram

Как работает курсор: модификация данных

//Type-safe, camelCase API
item.setDefaultPrice(14.9);
item.update();              //<---
Diagram
update item set default_price=14.9 where id = 42;

Как работает курсор: модификация данных

//Type-safe, camelCase API
item.setDefaultPrice(14.9);
item.update();
Diagram

Как работает курсор: удаление

item.setId(43).delete();
Diagram
delete from item where id = 43;

Как работает курсор: вставка с возвратом полей, вычисляемых базой данных

item.setName("cheese").insert();
Diagram

Как работает курсор: вставка с возвратом полей, вычисляемых базой данных

item.insert();
Diagram

Как работает курсор: чтение фильтрованной выборки

//Не происходит запроса к БД, просто конфигурируем курсор
cursor.setRange(cursor.COLUMNS.foo(), value)

//SELECT ... WHERE foo = value
for (MyCursor rec: cursor) {
    ....
}

Защита от потерянных обновлений

var rec = new ItemCursor(ctx);
rec.get(42);
rec.setName("cheese")
   .update();
var rec = new ItemCursor(ctx);
rec.get(42);
rec.setName("bread")
   .update();
  1. Каждая таблица содержит поле recversion

  2. Каждая таблица имеет UPDATE-триггер, проверяющий версию прочитанных данных и инкрементирующий recversion.

  3. Отключаемо при помощи WITH NO VERSION CHECK

"Проблема N+1"

Diagram
OrderLineCursor line = new OrderLineCursor(ctx);
ItemCursor item = new ItemCursor(ctx);
for (var l: line) {
  item.get(l.getItemId());
/* достаем данные в каждой итерации :-( */
}

"Проблема N+1": чуть лучше

Diagram
OrderLineCursor line = new OrderLineCursor(ctx);
ItemCursor item = new ItemCursor(ctx);
for (var l: line) {
  /* Objects.equals защищает от NPE */
  if (!Objects.equals(item.getId(), l.getItemId()))
  /* меньше обращений к базе */
    item.get(l.getItemId());
}

"Проблема N+1": оптимальный вариант

Diagram
OrderLineCursor line = new OrderLineCursor(ctx);
ItemCursor item = new ItemCursor(ctx);
// Сортируем по join колонке
line.orderBy(line.COLUMNS.itemId());
for (var l: line) {
  /* Objects.equals защищает от NPE */
  if (!Objects.equals(item.getId(), l.getItemId()))
  /* меньше обращений к базе */
    item.get(l.getItemId());
}

Проблемы N+1 может и не быть :-)

create view item_view as
    select
      i.id as id,
      i.name as name,
      o.ordered_quantity as ordered_quantity,
      o.ordered_amount as ordered_amount
    from item as i left join item_orders as o on i.id = o.item_id;
  • ItemViewCursor содержит все нужные поля

Проблема чтения лишних полей

RecCursor rec = new RecCursor(context);
rec.get(42)
Diagram

Проблема чтения лишних полей

RecCursor.Columns columns = new RecCursor.Columns(celesta);
RecCursor rec = new RecCursor(context, columns.f1(), columns.f3());
rec.get(42)
Diagram

Как работает транзакция

  • CallContext — короткоживущий объект, несущий информацию о связи с базой данных, текущей транзакции и всех ресурсах, задействованных в текущей транзакции

@CelestaTransaction
public OrderDTO postOrder(CallContext ctx, OrderDTO orderDTO) {
    //каждому курсору нужен контекст
    OrderCursor orderCursor = new OrderCursor(ctx);
    //делаем что-то с базой
    ....

}

В это время снаружи транзакции…​

try {
    ctx.activate(celesta, ....);
    Object result = joinPoint.proceed(); //<--
    ctx.commit();
    return result;
} catch (Throwable e) {
    ctx.rollback();
    throw e;
} finally {
    ctx.close();
}

Пример: схема данных

dataschema

Пример: Архитектура

Diagram

Пример: Архитектура

Diagram

Выводы

  • Database-first разработка

  • Фокус только на бизнес-логику и её тестирование

  • Быстрые и простые тесты, как Unit, так и E2E

Спасибо за внимание!

  • Понравилось? Поставьте звезду!

  • Помогите советом

  • Попробуйте уже, наконец!

@inponomarev