Попробуйте, наконец, 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 типами баз данных. Код не изменяется при замене базы данных!

diag cc562e4daa4dc11a4f3fc642f6d56458

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);
diag f8b2bb4b4994fef8b0453a818a14bffe
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);
diag e0fbebfd47defe2d4de04267fde8aaec
select id, name, default_price from item where id = 42 limit 1;

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

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

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

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

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

//Type-safe, camelCase API
item.setDefaultPrice(14.9);
item.update();
diag 8a22e77583541ef9e703cb6add2cb697

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

item.setId(43).delete();
diag 33f816eb77f82c720f2f6eadf3295243
delete from item where id = 43;

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

item.setName("cheese").insert();
diag 93cc9e7a6476e4e0be40b68d78d80270

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

item.insert();
diag d04d4d205f461d3f7a06099a71b2bc81

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

//Не происходит запроса к БД, просто конфигурируем курсор
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"

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

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

diag b653ab6d9a234d8d26a66042d9a40ef7
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": оптимальный вариант

diag b653ab6d9a234d8d26a66042d9a40ef7
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)
diag 764735064fbb327500777525c62b4cc7

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

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

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

  • 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

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

diag fc6f9e9cccb2ad0f2ddd7a8cded73e35

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

diag 112a3196d5c040dcf6bd5dcc9077e6de

Выводы

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

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

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

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

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

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

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

@inponomarev