Почему Kora не нужен ORM для большинства сервисов¶
15 сентября 2026
В разработке бэкендов на Java существует знакомый аргумент: если приложение общается с реляционной базой данных, оно, вероятно, должно использовать ORM. Рассуждение звучит очевидно. SQL повторяется, JDBC многословен, объектный маппинг утомителен, а инфраструктура персистентности уже решена зрелыми библиотеками. Долгое время это делало персистентность на основе ORM выбором по умолчанию для многих серверных приложений.
Фреймворк Kora занимает другую позицию. Он не утверждает, что ORM плохи, устарели или не нужны в каждом приложении. Он приводит более узкий и более практичный аргумент: для большого класса бэкенд-сервисов дорогая часть прямого доступа к базе данных — не сам SQL. Дорогая часть — это механический код вокруг SQL. Если фреймворк может генерировать этот механический код на этапе компиляции, то сервис может сохранить явный SQL и прямолинейный Java или Kotlin, не принимая полную модель объектной персистентности.
Это приводит к намеренно простому уравнению:
Репозитории Kora позволяют разработчику написать запрос, объявить типизированный метод репозитория и позволить фреймворку сгенерировать реализацию JDBC, привязку параметров, маппинг строк, обработку ресурсов, интеграцию телеметрии и участие в транзакциях. Kora 2 затем сочетает эту модель с синхронным кодом приложения и виртуальными потоками. Результат — архитектура, которая на поверхности выглядит почти старомодной:
простой синхронный репозиторий
+
JDBC
+
виртуальные потоки
=
высокая конкурентность без реактивного API базы данных
Но модель времени выполнения под этим сильно отличается от старого мира «платформенный поток на запрос», который изначально подтолкнул многие системы к реактивному доступу к базе данных. Виртуальные потоки делают блокирующий код дешёвым для парковки. Сгенерированные репозитории Kora убирают большую часть повторяющейся церемонии JDBC. Пул соединений с базой данных остаётся реальной границей конкурентности, где он, по сути, и должен был быть всё это время.
Интересный вопрос поэтому не «ORM хорош или плох?» Полезный вопрос: какие проблемы решает ORM, какие из этих проблем реально есть у этого сервиса и какие из них можно решить дешевле сгенерированными репозиториями и явным SQL?
Для многих микросервисов, внутренних API, обработчиков событий, транзакционно-ориентированных сервисов, CRUD-бэкендов и компонентов приложений, работающих с данными, ответ Kora в том, что полный ORM часто решает больше проблем, чем есть у сервиса.
ORM решает реальную проблему¶
Любое серьёзное сравнение должно начаться с признания заслуг ORM. Сырой JDBC неприятен в масштабе. Разработчику традиционно приходится получать соединение, создавать подготовленный оператор, привязывать параметры по числовым индексам, выполнять его, перебирать ResultSet, обрабатывать null-значения, преобразовывать типы JDBC в типы приложения, корректно закрывать ресурсы, переводить исключения, координировать транзакции и добавлять инструментирование. SQL-запрос из десяти строк может потребовать несколько десятков строк Java.
ORM убрали большую часть этой повторяющейся работы. Они также ввели мощные высокоуровневые функции: идентичность сущностей, контексты персистентности, автоматическую проверку изменений, каскады, навигацию по связям, ленивую загрузку, оптимистичную блокировку, кеши второго уровня, абстракцию запросов, маппинг схем и семантику единицы работы. В приложениях, построенных вокруг богатой персистентности агрегатов, эти возможности могут быть по-настоящему ценными.
Проблема начинается, когда вся эта модель становится ответом по умолчанию для приложений, которым большая её часть на самом деле не нужна. Сервис, который в основном выполняет явные проекции чтения, условные обновления, вставки, небольшие транзакции и горстку специфичных для базы данных запросов, может не выиграть от управляемого графа объектов. Ему может просто понадобиться лаконичный способ безопасно выполнять SQL и преобразовывать результаты в типизированные значения.
Kora фокусируется на этой более узкой потребности.
Фундаментальное несоответствие импеданса¶
Фраза «несоответствие импеданса объектно-реляционной модели» стара, но лежащая в основе проблема не исчезла. Реляционная база данных представляет информацию с помощью таблиц, строк, столбцов, ключей, ограничений, соединений, множеств и декларативных запросов. Java-приложения представляют информацию с помощью классов, записей, интерфейсов, ссылок, коллекций, методов и объектной идентичности. Это не одна и та же модель.
Реляционная связь может выражаться через внешний ключ, в то время как объектная модель может предоставлять прямую ссылку. Разница кажется небольшой, пока в картину не входит жизненный цикл. Уже ли загружен связанный объект? Следует ли загрузить его немедленно? Должен ли фреймворк создать прокси? Что происходит вне сессии персистентности? Запускает ли сериализация запрос? Если связь меняется, какой SQL следует генерировать? Что если две объектные ссылки представляют одну и ту же строку? Что если запросу нужно только три столбца, а сущность содержит сорок?
ORM пытается соединить эти модели, создавая богатый слой персистентности между ними. Kora идёт другим путём: он не пытается заставить реляционную модель выглядеть как граф объектов. Он рассматривает запрос к базе данных как операцию базы данных и преобразует результат в обычные значения приложения.
Вместо конвейера, где реляционные данные становятся моделью сущностей ORM, затем управляемым графом, затем значениями приложения, общий путь Kora ближе к «результат SQL, сгенерированный маппер строк, значение Java или Kotlin». Несоответствие не «решено» притворством, что модели идентичны. Граница сделана явной и механически дешёвой.
Репозиторию не нужно быть менеджером сущностей¶
В Kora метод репозитория может объявлять явный запрос и обычное типизированное возвращаемое значение. Разработчик определяет операцию базы данных и результат на уровне Java. Сгенерированная реализация делает скучную работу.
Нет требования, чтобы возвращаемое значение было управляемым персистентным объектом. Это может быть неизменяемая запись. Если код позже создаёт другой экземпляр с тем же идентификатором, нет карты идентичности на уровне фреймворка, которая пытается решить, являются ли эти объекты одной и той же персистентной сущностью. Создание изменённой копии не обновляет базу данных автоматически. База данных меняется, когда код вызывает операцию репозитория, которая выполняет обновление.
Это более простая семантическая модель: вызов репозитория равен операции базы данных. Она заменяет более косвенную связь, где мутация объекта, состояние контекста персистентности и время сброса в конечном счёте становятся операцией базы данных. Для многих рабочих нагрузок сервисов явную модель легче понимать.
Сессия персистентности мощна, но не бесплатна¶
Одна из центральных концепций традиционной архитектуры ORM — это сессия персистентности или контекст персистентности. Она даёт полезные гарантии. Фреймворк может поддерживать идентичность сущностей внутри сессии. Он может отслеживать загруженные объекты, замечать изменения, откладывать SQL до сброса, пакетировать часть работы, управлять связями и предоставлять абстракцию единицы работы.
Это ценно, когда приложение действительно хочет манипулировать персистентным графом объектов. Но сессия также является скрытой машиной состояний. Разработчик должен понимать, является ли сущность транзиентной, управляемой, отсоединённой или удалённой, и какие операции перемещают её между этими состояниями. Они должны понимать, когда выдаётся SQL, что происходит, когда к ленивой связи обращаются после закрытия сессии, запускает ли запрос сброс и разрешаются ли две ссылки в одну управляемую идентичность.
Ни одна из этих концепций не плоха по своей природе. Это цена абстракции. Вопрос в том, нужно ли сервису платить эту цену.
Репозитории Kora обычно не вводят эквивалентную сессию персистентности. Запрос возвращает обычные значения. У этих значений нет скрытого жизненного цикла базы данных. У соединения и транзакции есть жизненный цикл; у объектов его нет. Для сервисов, ориентированных на запросы, это часто отображается более прямо на то, что код на самом деле делает.
Скрытые запросы — это операционная стоимость¶
Одна из самых распространённых критик ORM — скрытые запросы. Эту фразу иногда используют несправедливо, потому что опытные пользователи ORM могут писать очень явные запросы и тщательно контролировать планы выборки. Тем не менее, модель персистентности делает возможным ввод-вывод базы данных в местах, где исходный код не выглядит видимо как ввод-вывод базы данных.
Доступ к свойству связанного объекта может быть чисто чтением из памяти или может запускать SQL в зависимости от маппинга и состояния сессии. Сам исходник не всегда говорит вам, что именно. Во время проверки кода рецензент не всегда может определить количество запросов по поиску вызовов репозиториев. Во время сериализации обход графа объектов может случайно инициализировать связи. Во время отладки, казалось бы, невинный геттер может стать причиной дополнительного кругого путешествия к базе данных.
С явной моделью репозиториев Kora операции базы данных, как правило, остаются видимыми как вызовы репозиториев, или запрос репозитория может получить ровно те данные, которые нужны случаю использования, за одну операцию. Фреймворк не мешает разработчикам писать слишком много запросов. Он делает эти запросы труднее скрыть.
Проблема N+1 — на самом деле проблема видимости¶
Проблема запросов N+1 часто представляется как дефект, специфичный для ORM. Это не так. Любое приложение может выполнить один запрос для загрузки N строк, а затем выполнить ещё один запрос для каждой строки. Проблема в том, что обход ленивых связей может сделать дополнительные вызовы менее очевидными.
В явной архитектуре репозиториев тот же анти-паттерн содержит видимые вызовы базы данных. Это облегчает его замечание при проверке кода и облегчает рассуждения во время работы над производительностью. Что более важно, Kora поощряет проекции, специфичные для запроса. Если случаю использования нужна информация о пользователе плюс агрегаты заказов, напишите SQL, который просит базу данных об этом результате, а не восстанавливайте его через навигацию по объектам.
Реляционная база данных хороша в соединениях, группировке, агрегации и операциях над множествами. Репозиторий может преобразовать результат напрямую в проекцию, спроектированную для этого случая использования. План запроса становится частью дизайна случая использования, а не эмерджентным свойством обхода сущностей.
Модели, специфичные для запроса, уменьшают несоответствие вместо того, чтобы прятать его¶
Основной источник сложности ORM — предположение, что должно быть одно первичное объектное представление строки базы данных. Сущность User может содержать идентичность, данные профиля, связи, разрешения, поля аудита и другое состояние. Большинству запросов не нужно всё это.
Одной конечной точке API может понадобиться только id и name. Другой может понадобиться id и permissions. Отчёту может понадобиться организация и количество пользователей. Результату поиска может понадобиться соединённая проекция из нескольких таблиц. ORM поддерживают DTO- проекции и графы сущностей, но ментальная модель по умолчанию, ориентированная на сущности, часто поощряет команды начинать с «загрузи сущность» и оптимизировать ненужное состояние позже.
У модели репозиториев Kora нет причин привилегировать одну управляемую сущность. Метод репозитория может возвращать ровно ту форму, которая требуется случаю использования: полное значение, компактную сводку, соединённую проекцию, скаляр, булево значение или счётчик обновлений. Модель Java описывает, что возвращает этот запрос. SQL описывает, как база данных его вычисляет. Kora генерирует маппинг.
Отслеживание изменений удобно, пока вам не нужно знать, что записывает¶
Проверка изменений — одна из самых полезных функций ORM. Внутри транзакции код приложения может загрузить сущность, изменить свойство и положиться на провайдера персистентности для обнаружения разницы и создания обновления.
Для доменных моделей, которые естественно ведут себя как изменяемые персистентные агрегаты, это может сделать код приложения выразительным. Но отслеживание изменений также меняет то, как разработчики рассуждают о побочных эффектах. Сеттер выглядит как мутация в памяти, но может подразумевать будущую запись в базу данных. Фактическая запись может произойти позже во время сброса. Другие запросы могут сначала вызвать сброс. Слушатели могут менять дополнительное состояние. Оптимистичные версии могут обновляться.
Явный подход репозиториев Kora отдаёт предпочтение операциям, чей побочный эффект виден. Метод вроде markPaid или updateEmail говорит читателю, что происходит изменение базы данных. SQL показывает, какие столбцы меняются, и может включать условие конкурентности напрямую. Счётчик затронутых строк может показать, действительно ли операция совпала с ожидаемым состоянием.
Для сервисов, ориентированных на транзакции, эта явность часто ценнее, чем автоматическая проверка изменений.
Явные обновления могут лучше кодировать бизнес-конкурентность¶
Рассмотрим переход состояния, который разрешён только один раз. Общий рабочий процесс сущностей может загрузить строку, проверить её статус, изменить её и сбросить. Но база данных часто может выразить инвариант атомарно в одном условном обновлении. Если затронута одна строка, переход удался. Если затронуто ноль строк, текущее состояние не было подходящим.
Это устраняет гонку чтение-изменение-запись и может избежать ненужного select. Репозитории Kora делают такие операции естественными, потому что явный SQL — не запасной выход из модели персистентности. Это и есть модель персистентности.
Это важно для сервисов, ориентированных на команды, где многие записи лучше всего выражаются как атомарные операции базы данных, а не как изменения жизненного цикла объектов.
Кеш ORM — ещё один слой состояния¶
ORM могут поддерживать кеши первого и второго уровня. Эти механизмы могут быть полезны. Они также могут усложнять рассуждения о свежести, инвалидации, использовании памяти и поведении запросов.
У сервиса может уже быть Redis, Caffeine, HTTP-кеширование, буферный кеш базы данных, реплики или материализованные представления. Добавление ещё одного прозрачного кеша персистентности не автоматически полезно. Для многих микросервисов время жизни запросов короткое, и кеширование лучше выражать намеренно на границах приложения, где ключи, значения, TTL и инвалидация видны.
Модель репозиториев Kora не требует объектного кеша просто для выполнения доступа к данным. Кеширование остаётся отдельным архитектурным решением, а не неявным свойством персистентности.
Накладные расходы памяти — это больше, чем объекты сущностей¶
Важная разница в памяти не в том, что ORM выделяет объекты, а JDBC нет. Оба производят данные приложения. Дополнительные накладные расходы исходят из механизмов, необходимых для управления жизненным циклом сущностей: записи контекста персистентности, снимки сущностей для проверки изменений, объекты прокси, обёртки ленивых коллекций, карты идентичности, метаданные и очереди действий.
Для небольшой транзакции это может быть нерелевантно. Для больших наборов результатов или множества конкурентных операций это может стать заметнее. Модель результатов Kora проще. Сопоставленная строка может стать неизменяемой записью или классом данных без прикреплённого учёта персистентности. После возврата это просто данные приложения.
Это может снизить давление на память и, что не менее важно, уменьшить количество концепций фреймворка, прикреплённых к каждому объекту базы данных.
Проверка изменений на основе снимков имеет модель стоимости¶
Традиционная проверка изменений часто требует, чтобы фреймворк запоминал достаточно предыдущего состояния, чтобы знать, изменились ли поля. Эта возможность покупает прозрачную персистентность. Сервисы, которые в основном выполняют явные обновления, не нуждаются в том, чтобы фреймворк запоминал старое состояние объектов.
Операция репозитория уже говорит, что должно измениться. Никакой снимок не требуется для вывода SQL из более позднего сравнения объектов. Операция базы данных известна, когда вызывается метод.
Это повторяющаяся тема Kora: избегайте поддержания состояния времени выполнения, когда явные операции и структура на этапе компиляции уже содержат требуемую информацию.
Область сессии часто сложнее, чем область транзакции¶
У сессии персистентности есть семантика за пределами самой транзакции базы данных. Командам приходится решать, является ли сессия на транзакцию, на запрос или расширенной, и как ленивые значения ведут себя вне этой области. Исторические паттерны вроде Open Session in View держали сессии живыми достаточно долго, чтобы презентационный код мог обращаться к ленивым связям, но это также позволяло вводу-выводу базы данных происходить далеко от слоя репозиториев или сервисов.
Модель JDBC Kora фокусируется на более конкретной концепции: границе соединения и транзакции. Внутри транзакции вызовы репозиториев разделяют транзакционное соединение. Вне её вызовы репозиториев — независимые операции. Возвращённые значения не нуждаются в сессии после завершения запроса.
Для сервисных приложений этот жизненный цикл часто легче объяснить.
Транзакции должны быть об операциях базы данных¶
Транзакция JDBC Kora может быть выражена напрямую вокруг операций: вставить заказ, вставить позиции, вставить строку outbox, зафиксировать. Важный факт виден в исходнике: эти запросы разделяют одну транзакцию.
Нет необходимости рассуждать о том, грязная ли сущность, когда она сбросится или меняет ли обращение к связи последовательность SQL. Транзакция содержит действия базы данных, а не управляемое состояние объектов.
Эта модель особенно интуитивна для сервисных рабочих процессов, паттернов outbox, условных обновлений и явной обработки команд.
Семантика сброса ORM может удивлять в иначе простом коде¶
В ORM запрос может вызвать сброс ожидающих изменений сущностей до того, как запрос выполнится, чтобы результаты оставались согласованными с управляемым состоянием. Это поведение логично, но означает, что операции, похожей на чтение, могут предшествовать обновления в зависимости от более ранней активности сессии.
Опытные пользователи ORM это понимают. Проблема не в корректности. Проблема в том, что связь на уровне исходника между операцией и SQL косвенна.
В явной модели Kora обновление происходит, потому что был вызван метод обновления репозитория. Select происходит, потому что был вызван метод select репозитория. Это более тесное соответствие делает производительность и порядок транзакций легче проверять.
Ленивая загрузка — это компромисс, а не магия¶
Ленивая загрузка решает реальную проблему: не загружайте данные, пока они не понадобятся. Но она делает это, превращая навигацию по объектам в потенциальный ввод-вывод. Доступ к полю может стать операцией базы данных.
Для локальных объектов приложения разработчики обычно ожидают, что доступ к свойству будет дешёвым и детерминированным. Ленивый персистентный прокси меняет это ожидание.
Kora предпочитает явный дизайн запросов для обычных сервисов. Если операции нужен заказ плюс клиент, напишите соединение или проекцию. Если нужен только заказ, не выбирайте клиента. Форма запроса сообщает стратегию выборки напрямую. Нет отдельного слоя конфигурации ленивой/жадной загрузки, который нужно поддерживать.
Kora убирает главную проблему эргономики JDBC¶
Самый сильный исторический аргумент против сырого JDBC не в том, что сам SQL неприятен. Это окружающий код: try-with-resources, создание подготовленных операторов, числовые индексы параметров, перебор набора результатов, преобразование null, преобразование типов, телеметрия и обработка исключений.
Напишите это сотни раз — и проект станет фабрикой шаблонного кода.
Kora убирает ровно этот код через генерацию. Разработчик сохраняет части, которые стоит проверять: запрос, сигнатуру метода, тип возврата, границу транзакции. Фреймворк генерирует церемонию.
Это существенно меняет компромисс. Kora не просит разработчиков выбирать между Hibernate и ручной JDBC-обвязкой. Он предлагает третий вариант: явный SQL со сгенерированной склейкой выполнения.
Сгенерированные репозитории — это специализированный код¶
На этапе компиляции Kora знает метод репозитория, текст запроса, имена параметров, типы параметров, тип возврата, модель строк, пользовательские мапперы и компонент базы данных. Этого достаточно, чтобы сгенерировать специализированную реализацию.
Времени выполнения не нужно интерпретировать общий метод репозитория при каждом вызове. Обработка аннотаций или KSP производит обычную инфраструктуру Java или Kotlin, которая затем компилируется вместе с приложением.
Это согласуется с более широкой архитектурой Kora. Внедрение зависимостей, HTTP-адаптеры, границы OpenAPI, репозитории, маппинги и аспекты следуют одному принципу: переносите предсказуемую структурную работу в компиляцию и держите время выполнения тонким.
Маппинг строк генерируется, а не повторяется¶
Kora может генерировать маппинг строк для типизированных представлений, таких как Java-записи или Kotlin- классы данных. Вместо рефлексивного обнаружения конструкторов или ручного написания циклов по наборам результатов для каждого запроса сгенерированный маппер может напрямую читать столбцы и создавать значение.
Пользовательские мапперы остаются доступными для JSONB, массивов, перечислений, пользовательских идентификаторов, специфичных для вендора типов и других случаев, требующих явного преобразования.
Маппинг остаётся первоклассной задачей, но не требует полной сессии персистентности или движка рефлексии времени выполнения.
Макросы запросов убирают структурное повторение¶
У явного SQL есть одна повторяющаяся слабость: списки столбцов и списки полей вставки/обновления могут стать утомительными в сопровождении. Макросы запросов Kora нацелены ровно на эту проблему.
Макрос select может вывести поля возвращаемого представления. Макрос insert может вывести столбцы и именованные параметры входного представления. Макросы, учитывающие идентификаторы, могут исключать сгенерированные ID или строить предикаты.
Важный момент в том, чего макросы не делают. Они не заменяют SQL языком запросов фреймворка. Они убирают механические фрагменты, сохраняя соединения, фильтры, группировку, сортировку, блокировки, CTE и синтаксис вендора в обычном SQL.
Это узкая абстракция с небольшим семантическим разрывом.
Видимость SQL ускоряет отладку производительности¶
Когда репозиторий Kora медленный, последовательность отладки прямая: определите метод репозитория, прочитайте SQL, запустите EXPLAIN или EXPLAIN ANALYZE, проверьте индексы и оценки строк, затем измените запрос или схему.
С более абстрактным слоем персистентности последовательность может включать определение сгенерированного SQL, плана выборки, поведения сброса и фактического количества запросов до того, как расследование базы данных вообще начнётся.
Хороший инструментарий ORM может сделать эти шаги управляемыми. Kora просто убирает их из пути по умолчанию.
Для команд, которые владеют производительностью базы данных, эта прозрачность часто является большим преимуществом.
Нативные функции базы данных перестают быть запасными выходами¶
Реляционные базы данных предоставляют мощные возможности, которые не отображаются аккуратно на универсальную объектную модель. PostgreSQL предлагает RETURNING, ON CONFLICT, JSONB, массивы, оконные функции, CTE, SKIP LOCKED, консультативные блокировки и специализированные операторы.
ORM поддерживают многие функции вендора через механизмы, специфичные для провайдера, или нативные запросы. В Kora использование этих функций не означает выход из предполагаемой модели персистентности. Предполагаемая модель уже начинается с нативного SQL.
Запрос захвата работы может использовать SKIP LOCKED напрямую. Upsert может использовать ON CONFLICT. Обновление может использовать RETURNING. Фреймворку не нужно изобретать промежуточную абстракцию для каждой функции базы данных, прежде чем приложение сможет её использовать.
Переносимость базы данных обычно переоценивается¶
Есть системы, где поддержка нескольких реляционных баз данных — это подлинное продуктовое требование. В таких системах переносимая абстракция может иметь реальную ценность.
Многие производственные сервисы, однако, не реалистично переносимы на уровне базы данных. Они зависят от миграций, типов данных, индексов, планов запросов, поведения блокировок, расширений, репликации и операционного инструментария. Переход с PostgreSQL на другую базу данных обычно является архитектурной миграцией, а не изменением строки соединения.
Если переносимость не является реальным требованием, избегание специфичных для базы данных возможностей лишь для сохранения теоретической гибкости может налагать реальную стоимость ради гипотетической выгоды.
Kora держит базу данных видимой и позволяет командам решать, насколько переносимыми им на самом деле нужно быть.
Одна модель репозитория не означает одну абстракцию базы данных¶
Kora предоставляет общую модель программирования репозиториев для JDBC и Cassandra, но не притворяется, что эти базы данных взаимозаменяемы.
Общие концепции — репозиторий, запрос, типизированные параметры, типизированные результаты, маппинг, пакетирование и телеметрия. Семантика реализации остаётся нативной.
У JDBC есть SQL, соединения, подготовленные операторы, наборы результатов, транзакции и сгенерированные ключи. У Cassandra есть CQL, CqlSession, связанные операторы, моделирование данных с учётом партиций, профили выполнения и специфичная для Cassandra семантика согласованности.
Это правильный уровень абстракции: унифицируйте механику разработчика, сохраняйте реальность технологии.
Cassandra показывает, почему мышление ORM не обобщается¶
Таблицы Cassandra спроектированы вокруг паттернов доступа. Денормализация — это норма. Ключи партиций и ключи кластеризации определяют жизнеспособность запросов. Соединения между таблицами — не центральная модель.
Модель репозиториев, ориентированная на запросы, подходит естественно, потому что Kora не требует реляционного графа сущностей внизу. Разработчик пишет CQL, объявляет типизированный метод, и Kora генерирует привязку и маппинг строк.
Та же эргономика сохраняется. Семантика базы данных не стирается.
Большинству сервисов не нужен один канонический тип сущности¶
Без контекста персистентности нет давления заставить один класс обслуживать каждый случай использования базы данных.
Сервис может иметь UserRow, UserSummary, UserCredentials, UserSearchResult, CreateUser, UpdateUserEmail и другие типы, созданные под конкретные цели. Это не обязательно избыточно. Они представляют разные контракты.
Результат поиска может объединять несколько таблиц. Сводка может содержать два столбца. Команда создания может опускать сгенерированный базой данных идентификатор. Запись может возвращать только счётчик обновлений.
Этот стиль «сначала проекция» часто производит код, более близкий к реальным случаям использования, чем один канонический граф сущностей.
Использование памяти становится легче контролировать¶
Проекции, специфичные для запроса, помогают поведению памяти. Если конечной точке нужны три столбца, она может выбрать и материализовать три столбца. Ей не нужно загружать полную сущность с двадцатью полями или создавать ленивые прокси для связей, к которым она, вероятно, не будет обращаться.
Это не гарантирует низкую память; явный SQL-сервис всё ещё может небрежно загружать огромные наборы результатов. Преимущество в том, что фреймворк не навязывает больший граф объектов, чем запросил запрос.
Память остаётся ближе к явному дизайну доступа к данным.
Кеш второго уровня — не автоматически функция¶
Кеширование должно отвечать на конкретную проблему согласованности и производительности. Сервис может выбрать Redis, Caffeine, HTTP-кеширование, материализованные представления или реплики базы данных в зависимости от случая использования.
Кеш второго уровня ORM может быть мощным, но он также создаёт ещё одну область инвалидации. Когда сервис уже использует явные бизнес-модели чтения, кеш уровня приложения может быть легче определить и наблюдать.
Kora держит кеширование и персистентность как отдельные задачи. Сервис добавляет кеш, потому что от него выигрывает случай использования, а не потому что персистентность сущностей приносит с собой подсистему кеша.
Простые значения легче сериализовать¶
Управляемые сущности ORM могут содержать ленивые связи или прокси времени выполнения, чьё поведение зависит от состояния сессии. Сериализация их напрямую через HTTP- API может запускать дополнительные запросы, раскрывать внутренние связи, рекурсивно обходить неожиданно или завершаться ошибкой после закрытия сессии.
Зрелые приложения ORM часто решают это, вводя DTO в любом случае.
Результаты репозиториев Kora уже являются обычными значениями. Они могут быть записями, специфичными для запроса, или преобразованы в сгенерированные из OpenAPI модели ответов. Контракт HTTP и контракт персистентности остаются раздельными.
Это согласуется с более широкой философией Kora «сначала контракт».
Явный SQL не означает, что схема базы данных диктует Java¶
Маппинг Kora поддерживает переопределения имён столбцов, встроенные значения, пользовательские мапперы и типизированные обёртки. Java и Kotlin всё ещё могут использовать осмысленные объекты значений, а не механически отражать столбцы таблиц.
Модели базы данных и приложения могут различаться. Kora просто генерирует мост, а не поддерживает персистентную машину состояний управляемых объектов вокруг модели приложения.
Это важное различие: отказ от ORM не означает отказ от маппинга или доменного дизайна.
Виртуальные потоки меняют исторический компромисс JDBC¶
Самая важная связь Kora 2 — виртуальные потоки.
Почему реактивный доступ к базе данных стал привлекательным? Одна из главных причин — это стоимость блокировки платформенных потоков. Традиционная серверная архитектура привязывала запрос к Java-потоку, поддерживаемому операционной системой. Когда этот поток выполнял JDBC и ждал базу данных, поток оставался недоступным для другой работы.
При тысячах или десятках тысяч конкурентных ожиданий ввода-вывода стеки платформенных потоков и накладные расходы планировщика становились дорогими. Реактивные архитектуры решали это, представляя асинхронную работу без выделения одного тяжёлого потока каждому ожидающему запросу.
Это была реальная инженерная проблема.
Виртуальные потоки меняют именно это ограничение.
Виртуальные потоки делают блокировку дешёвой для парковки¶
Виртуальный поток по-прежнему является Java Thread с точки зрения приложения, но он не постоянно привязан к одному потоку-носителю. Во время поддерживаемого блокирующего ввода-вывода JVM может припарковать виртуальный поток и позволить носителю выполнять другую задачу.
Код приложения поэтому может оставаться прямолинейным: вызвать репозиторий, дождаться результата, продолжить. Пока операция JDBC ждёт, виртуальный поток может быть припаркован. Носитель свободен для выполнения других виртуальных потоков.
Это восстанавливает эргономическую ценность программирования «поток на запрос» без требования одного тяжёлого платформенного потока для каждого выполняющегося запроса.
Это фундамент синхронного направления Kora 2.
Блокировка виртуального потока — не то же самое, что блокировка носителя¶
Желаемое поведение: виртуальный поток ждёт, носитель становится доступным. Это фундаментально отличается от исторической модели, где платформенный поток запроса ждал и оставался недоступным.
Однако некоторые операции могут закреплять виртуальные потоки на носителях, а нативный код или паттерны синхронизации могут влиять на поведение. Библиотеки всё ещё нуждаются в производственной валидации.
Виртуальные потоки снижают стоимость ожидания. Они не делают каждую форму блокировки универсально бесплатной.
JDBC естественно совместим с синхронной моделью¶
JDBC синхронен: выполнить запрос, подождать, получить результат. Это исторически создавало напряжение с серверами на основе event-loop, потому что прямой вызов JDBC в потоке event-loop остановил бы несвязанную работу.
Традиционные решения заключались в выгрузке JDBC в пул воркеров или использовании реактивного драйвера базы данных.
Виртуальные потоки вводят ещё один вариант: выполнять код приложения на виртуальном потоке, вызывать обычный JDBC и позволять виртуальному потоку парковаться во время ввода-вывода.
Это отлично согласуется с API репозиториев Kora. Сигнатуры репозиториев остаются синхронными. Драйверы базы данных остаются зрелыми JDBC-драйверами. Поток управления приложения остаётся обычным Java или Kotlin. Конкурентность масштабируется через виртуальные потоки, а не через реактивные типы возврата.
Простой синхронный репозиторий + JDBC + виртуальные потоки¶
Архитектуру можно резюмировать напрямую:
простой синхронный репозиторий
+
JDBC
+
виртуальные потоки
=
высокая конкурентность без реактивного API базы данных
Каждая часть важна. Простой синхронный репозиторий даёт вызывающим обычные значения, а не publisher'ов или future. JDBC предоставляет зрелую реляционную Java-экосистему. Виртуальные потоки означают, что ожидание JDBC больше не требует выделения тяжёлого платформенного потока на всё время ожидания.
Вместе эти свойства убирают большую часть исторической мотивации для введения реактивных API баз данных в обычные сервисы запрос-ответ.
Реактивные API баз данных всё ещё имеют законное применение¶
Реактивный доступ к базе данных не устарел. Он остаётся ценным для рабочих нагрузок, построенных вокруг сквозной потоковой передачи, явного противодавления, реактивной композиции через асинхронные системы или библиотек и драйверов, чья реактивная семантика даёт реальную ценность.
Аргумент уже: сервису не должен требоваться реактивный API базы данных только потому, что блокировка платформенного потока была слишком дорогой.
Виртуальные потоки меняют эту стоимость. Если приложение естественно вписывается в синхронный запрос-ответ и транзакционный поток, JDBC снова становится привлекательным при уровнях конкурентности, которые когда-то подталкивали команды к реактивному программированию.
Реактивная сложность — это инженерная стоимость¶
Реактивные API меняют структуру приложения. Вместо прямолинейного последовательного потока управления разработчики работают с операторами, подписками, границами планировщиков, распространением контекста, отменой, противодавлением и асинхронными стек-трейсами.
Опытные реактивные команды хорошо с этим справляются. Если рабочая нагрузка действительно выигрывает от реактивной семантики, сложность оправдана.
Если единственная цель — избежать заблокированных платформенных потоков, виртуальные потоки дают гораздо более простой ответ. Kora 2 намеренно предпочитает эту более простую модель для общего пути.
Транзакционный код легче читать синхронно¶
Транзакции часто содержат зависимые шаги: вставить заказ, использовать сгенерированный идентификатор, вставить позиции, вставить событие outbox, вернуть результат.
Синхронный код выражает это естественно внутри одного блока транзакции. С реактивным доступом к базе данных контекст транзакции должен проходить через асинхронную композицию корректно. Библиотеки это поддерживают, но ментальная модель больше.
Виртуальные потоки делают синхронный транзакционный код масштабируемым, не заставляя саму транзакцию входить в реактивный граф.
Для бизнес-сервисов, где границы транзакций центральны для корректности, эта простота важна.
Пул соединений с базой данных всё ещё является реальным пределом¶
Виртуальные потоки могут создавать огромную конкурентность. База данных не обязательно может обработать огромную конкурентность.
У сервиса может быть десятки тысяч виртуальных потоков, но только несколько десятков JDBC-соединений. Только эти соединения могут активно выполнять операции базы данных одновременно. Остальные задачи ждут пул.
Это не сбой. Это предполагаемая граница ресурсов. У базы данных конечные CPU, память, блокировки, пропускная способность хранения и ёмкость соединений.
Виртуальные потоки убирают узкое место потоков приложения, сохраняя узкое место ресурсов базы данных. Это делает роль пула соединений яснее.
Безграничные виртуальные потоки не означают безграничную конкурентность базы данных¶
Виртуальные потоки делают ожидание дешёвым; они не делают ресурс, которого ждут, безграничным.
Пропускная способность базы данных по-прежнему определяется размером пула, задержкой запросов, длительностью транзакций, конкуренцией за блокировки, хранилищем, CPU и сетью. Если скорость поступления превышает ёмкость обслуживания, ожидающие виртуальные потоки могут накапливаться. Память дешевле стеков платформенных потоков, но поставленные в очередь запросы всё равно сохраняют состояние запроса и увеличивают задержку.
Системе всё ещё нужны таймауты, сброс нагрузки, планирование ёмкости и разумная политика повторов.
Виртуальные потоки упрощают механику конкурентности. Они не отменяют теорию очередей.
Закон Литтла всё ещё применяется¶
Для стабильной системы конкурентность примерно равна пропускной способности, умноженной на задержку. Если задержка увеличивается при перегрузке, та же пропускная способность требует большей конкурентности, что может увеличить очередь и давление дальше.
Виртуальные потоки облегчают приложению удержание многих ожидающих операций. Они не делают эту систему здоровой.
Вот почему синхронную модель Kora следует сочетать с производственным планированием ёмкости, а не интерпретировать как «блокировка бесплатна».
Пул становится явной границей противодавления¶
Ограниченный пул соединений JDBC естественно ограничивает количество операций базы данных, выполняющихся одновременно. Может существовать множество виртуальных потоков запросов, но только контролируемое число получает соединения с базой данных.
Это защищает базу данных только если само ожидание ограничено и наблюдаемо. Здоровому сервису нужны таймауты получения соединения, таймауты запросов, метрики насыщения пула и SLO задержки.
Простота синхронного кода не снимает необходимость в операционных контролях.
Не держите соединение с базой данных, ожидая в сети¶
Виртуальные потоки делают удалённое ожидание дешёвым для Java, но соединения с базой данных остаются дефицитными. Транзакция, которая выполняет обновление базы данных, затем ждёт удалённого HTTP-вызова, затем выполняет ещё одно обновление базы данных, может удерживать соединения и блокировки, не выполняя никакой работы базы данных.
Виртуальный поток может парковаться эффективно. Ресурсы базы данных — нет.
Здесь имеют значение такие паттерны, как outbox, saga, идемпотентный рабочий процесс или «транзакция-затем-удалённый-вызов». Виртуальные потоки оптимизируют планирование потоков, а не дизайн распределённых транзакций.
Длинные транзакции остаются длинными транзакциями¶
То же самое относится к дорогим вычислениям приложения внутри транзакции. Если транзакция открывается, выполняет один запрос, тратит секунды на работу CPU, а затем выдаёт ещё один запрос, соединение с базой данных остаётся занятым без необходимости.
Явная область транзакции Kora облегчает это увидеть при проверке. Правило остаётся простым: держите транзакции базы данных настолько малыми, насколько позволяет корректность.
Возвращение JDBC — на самом деле возвращение прямолинейного кода¶
Дело не в ностальгии по JDBC. Никто не хочет тысячи написанных вручную циклов по наборам результатов.
Привлекательная комбинация — зрелые JDBC-драйверы, сгенерированный шаблонный код репозиториев, конкурентность виртуальных потоков, интегрированная телеметрия и прямолинейный код приложения.
Компилятор Kora убирает старую проблему эргономики JDBC. Loom убирает большую часть старой проблемы масштабируемости потоков. Что остаётся — нативная модель базы данных, которую производственным инженерам всё равно приходилось понимать.
Вот почему Kora может построить современный высокопроизводительный бэкенд вокруг стиля программирования, который выглядит простым.
Реактивная эпоха решила реальное историческое ограничение¶
Реактивные серверные архитектуры были изобретены не потому, что разработчики хотели сложности. Они решали реальные ограничения платформы. Когда запросы потребляли тяжёлые потоки и сервисы проводили большую часть времени в ожидании баз данных или удалённых API, количество потоков становилось дорогим.
Асинхронные и управляемые событиями среды выполнения могли мультиплексировать огромное количество ожиданий ввода-вывода по небольшим пулам потоков. Реактивные библиотеки добавляли композицию и семантику противодавления.
Это был рациональный ответ на платформу, доступную в то время.
Виртуальные потоки не доказывают, что реактивная архитектура была ошибкой. Они меняют предположения, при которых команды делают этот выбор сегодня.
Kora 2 может предпочитать синхронные API, не отступая назад¶
Синхронно-первую модель Kora 2 не следует читать как «блокировка победила». Лучшая интерпретация в том, что JVM изменилась достаточно, чтобы похожий на блокировку исходный код больше не подразумевал один тяжёлый заблокированный поток операционной системы.
Модель исходника и модель времени выполнения разъединились. Код приложения может выглядеть последовательным, в то время как JVM планирует огромное количество виртуальных потоков по меньшему числу носителей.
Это позволяет дизайнерам фреймворков вернуть знакомые API без жертвы конкурентностью ввода-вывода. Kora агрессивно использует эту возможность.
Синхронные сигнатуры улучшают локальность API¶
Метод репозитория, возвращающий User, говорит ровно то, что нужно знать вызывающему. Вызывающий получает значение. Модель исключений обычна. Стек-трейс следует обычным вызовам. Транзакция может быть представлена обычной лексической областью.
Реактивный тип возврата сообщает дополнительную семантику выполнения, которая может быть полезна, но эта семантика распространяется через вызывающих. Если сервису не нужна потоковая передача или явная асинхронная композиция на этой границе, синхронная сигнатура имеет меньшую когнитивную нагрузку.
Виртуальные потоки делают эту сигнатуру с меньшей нагрузкой жизнеспособной при высокой конкурентности.
Отладка становится более знакомой¶
Прямолинейные синхронные вызовы обычно дают стек-трейсы, которые более тесно соответствуют структуре исходника. Во время инцидента инженер часто может видеть контроллер, сервис, репозиторий, JDBC.
Реактивный инструментарий существенно улучшился, но асинхронное выполнение всё ещё вводит другие паттерны отладки.
Для платформенных команд со многими обычными Java-разработчиками знакомая отладка имеет экономическую ценность. Более простая модель снижает стоимость обучения и диагностики инцидентов.
Зрелые JDBC-драйверы — крупный актив¶
JDBC представляет десятилетия инженерии у вендоров баз данных. Драйверы имеют зрелую поддержку аутентификации, подготовленных операторов, пакетирования, сгенерированных ключей, специфичных для базы данных типов, TLS, опций отработки отказа, интеграций наблюдаемости и пулов соединений.
Реактивные драйверы значительно улучшились, но выбор JDBC даёт командам самый устоявшийся путь для многих реляционных баз данных.
Если виртуальные потоки убирают причину масштабируемости потоков избегать JDBC, эта зрелость экосистемы становится крупным преимуществом.
Слой репозиториев Kora позволяет командам извлекать из него выгоду без принятия шаблонного кода сырого JDBC.
JDBC-транзакции широко понятны¶
Опытные Java-бэкенд-инженеры уже понимают соединение, auto-commit, commit, rollback и изоляцию. Это знание переносится между фреймворками.
Явная транзакция Kora находится близко к этой семантике. Это уменьшает объём специфичных для фреймворка знаний, требуемых для рассуждений об атомарности.
CTO или платформенная команда должны заботиться об этом, потому что переносимое знание улучшает адаптацию и мобильность команды.
ORM добавляет второй слой оптимизации¶
Когда приложение ORM медленное, есть как минимум две области оптимизации: поведение ORM и поведение базы данных.
Инженеру может понадобиться настраивать стратегию выборки, граф сущностей, размер пакета, область контекста персистентности, режим сброса или поведение кеша, а затем всё равно настраивать SQL, индексы, статистику и схему.
С репозиториями Kora большая часть работы по производительности начинается напрямую в области базы данных. Всё ещё есть конфигурация фреймворка, размер пула и маппинг, но семантический разрыв меньше.
Для платформенных команд, пытающихся стандартизировать практики производительности, меньшее количество слоёв может быть значимым преимуществом.
Явное кеширование даёт более продуманные SLO¶
Предположим, данные продукта кешируются на пять минут. Кеш уровня приложения может чётко указать ключ, значение, TTL и поведение инвалидации.
Если репозиторий остаётся детерминированным, границу кеша можно тестировать независимо. Это часто предпочтительнее опоры на семантику кеширования на уровне персистентности, оптимизированную вокруг сущностей, а не бизнес-моделей чтения.
У Kora есть выделенные абстракции кеша, поэтому кеширование можно добавлять там, где выигрывает случай использования, а не встраивать в модель персистентности.
Графы сущностей ORM могут вырастать за пределы запроса¶
Графы объектов удобны, потому что связанные данные ощущаются естественно навигируемыми. Они также могут облегчить загрузку большего количества данных, чем нужно запросу.
У пользователя есть заказы, у заказов есть позиции, у позиций есть продукты, у продуктов есть категории. Приложению в конечном счёте нужна стратегия выборки, чтобы решить, как далеко идёт обход.
Kora начинает с результата запроса. Если конечной точке нужны три соединённых значения, выберите три соединённых значения. База данных уже хороша в построении реляционных проекций. Нет необходимости сначала воссоздавать целый граф связей в памяти.
Отчёты и агрегация редко нуждаются в управляемых сущностях¶
Отчёты обычно нуждаются в SUM, COUNT, GROUP BY, оконных функциях, нескольких соединениях и группировке по датам. У результата может не быть содержательного представления сущности.
Kora может преобразовать агрегационный запрос напрямую в запись, созданную под конкретную цель. Никакой персистентной идентичности или жизненного цикла объектов не требуется.
Это одна из причин, почему сервисы с большим количеством чтения часто мало выигрывают от полной семантики ORM.
Поиск — та же история¶
Конечные точки поиска часто возвращают компактную проекцию, собранную из нескольких источников: id, отображаемый заголовок, владелец, статус, совпавший фрагмент.
Эти данные редко естественно живут как один управляемый агрегат. Явный запрос может собрать ровно нужную проекцию, а сгенерированный маппер может её создать. Никакой граф сущностей не требуется.
Модели чтения и записи могут чисто расходиться¶
Сервис может писать через компактные командные запросы и читать через оптимизированные проекции. Нет требования, чтобы обе операции разделяли один класс сущности.
Это совместимо с мышлением в стиле CQRS без требования тяжёлого фреймворка CQRS. API репозиториев просто отражают фактические случаи использования.
Большинство сервисов больше похожи на транзакционные скрипты, чем они признают¶
Большое число микросервисов имеют потоки приложения, напоминающие: проверить запрос, прочитать несколько строк, применить правило, записать одну или несколько строк, записать событие outbox, вернуть ответ.
Это ближе к сервису приложения или транзакционному скрипту, чем к манипуляции долгоживущим персистентным графом объектов.
Такой сервис может использовать сущности ORM, потому что ORM — организационный выбор по умолчанию, а не потому что рабочая нагрузка выигрывает от семантики контекста персистентности.
Для этих приложений репозитории Kora могут убрать значительную концептуальную механику, не делая код базы данных многословным.
CRUD сам по себе не является аргументом за ORM¶
Базовый CRUD лёг почти для любой технологии персистентности. С макросами Kora и сгенерированными мапперами CRUD-репозитории могут оставаться лаконичными, сохраняя SQL явным.
Если сервису нужен простой CRUD плюс несколько пользовательских запросов, введение сессий персистентности, отслеживания изменений, ленивых связей и семантики кеша может быть большей абстракцией, чем требует проблема.
Массовые обновления естественны без материализации сущностей¶
Предположим, сервис должен истечь все сессии старше отметки времени. Естественная операция базы данных — одно обновление по множеству.
Модель, ориентированная на сущности, тоже может выполнять массовые операции, но операция концептуально не должна загружать объекты.
Kora делает SQL на основе множеств первоклассным. База данных выполняет работу. Java получает счётчик обновлений.
Это часто проще и эффективнее.
Мышление на основе множеств — важный навык базы данных¶
Реляционные базы данных оптимизированы для операций над множествами. Объектно-ориентированный код естественно поощряет итерацию по объектам.
ORM соединяют эти два, но явный SQL держит мышление на основе множеств близко к дизайну приложения.
Перед запуском многих отдельных операций спросите, может ли база данных выполнить одно соединение, пакет, агрегацию или условное обновление вместо этого.
Этот образ мышления важнее самого фреймворка персистентности.
Виртуальные потоки могут заставить плохие паттерны базы данных падать быстрее¶
Поскольку виртуальные потоки делают конкурентность дешёвой, команды могут случайно распараллеливать неэффективные паттерны. Запуск одного виртуального потока на элемент не делает сотню кругых путешествий к базе данных хорошей идеей.
База данных всё ещё получает сотню операций. По многим запросам пул становится насыщенным.
Используйте SQL на основе множеств, пакетные API или лучший дизайн запросов, прежде чем использовать конкурентность для маскировки плохого паттерна доступа.
Виртуальные потоки делают легитимную конкурентность дешёвой. Они не делают неэффективную работу базы данных эффективной.
Пакетные API всё ещё важны¶
Для повторяющихся вставок или обновлений сгенерированный пакетный метод репозитория может уменьшить кругые путешествия и накладные расходы драйвера. Это часто предпочтительнее одного конкурентного вставления на строку.
Опять же, нативные для базы данных массовые механизмы следует использовать до конкурентности уровня приложения.
Kora поддерживает пакетирование, сохраняя запрос явным.
Оптимистичная блокировка не требует управляемой сущности¶
Оптимистичная блокировка часто ассоциируется с полем версии сущности, но лежащий в основе механизм — условное обновление.
Репозиторий может обновить строку, где id и ожидаемая версия совпадают, увеличить версию и проверить счётчик затронутых строк. Ноль строк означает конкурентную модификацию.
Это явно, естественно работает с неизменяемыми значениями и не требует состояния жизненного цикла персистентности.
Аннотация ORM удобна; это не единственная реализация оптимистичной конкурентности.
Пессимистичная блокировка более прозрачна в SQL¶
Аналогично, специфичное для базы данных предложение блокировки можно писать напрямую. Командам не нужно помнить, какой режим блокировки ORM отображается на какое поведение базы данных.
Для сервисов, намеренно стандартизированных на одном движке базы данных, нативный SQL часто сообщает механизм конкурентности более ясно.
Паттерн outbox естественно вписывается в явные репозитории¶
Надёжный поток публикации событий часто нуждается в обновлении бизнес-состояния и вставке записи outbox в одной транзакции. С Kora обе операции могут быть явными вызовами репозиториев внутри одной JDBC-транзакции.
Нет скрытого порядка сброса сущностей, о котором нужно рассуждать. Атомарность определяется вокруг операций базы данных напрямую.
Это отличный пример того, как простые синхронные репозитории компонуются с паттернами распределённых систем.
Виртуальные потоки не исправляют распределённые транзакции¶
Транзакция, которая держит соединение с базой данных, ожидая другой сервис, всё ещё рискованна. Виртуальный поток может парковаться дёшево, но блокировки и соединения остаются занятыми.
Распределённым рабочим процессам всё ещё нужна архитектура: outbox, saga, идемпотентность, компенсация или другой подходящий паттерн.
Более простую модель программирования Kora не следует путать с более простыми требованиями распределённых систем.
Количество запросов становится проверяемой метрикой¶
Поскольку доступ к базе данных тесно отображается на вызовы репозиториев, команды могут создавать прямолинейные ожидания производительности: эта конечная точка должна выполнять один вызов репозитория; этот рабочий процесс должен выполнять ноль запросов на элемент; этот пакетный путь должен оставаться ограниченным.
Телеметрия может использовать стабильные имена операций репозиториев, облегчая корреляцию исходника и производственных измерений.
Это ещё одна операционная выгода тонкого слоя данных.
Телеметрия принадлежит сгенерированной склейке¶
Написанный вручную JDBC часто начинается без согласованных трасс и метрик. Инфраструктура базы данных Kora может интегрировать телеметрию в сгенерированные вызовы репозиториев.
Это ровно тот вид сквозной задачи, которым генерация должна владеть. Разработчик пишет запрос. Платформа получает стандартизированное инструментирование.
Сложный ручной запрос всё ещё может оставаться внутри той же инфраструктуры телеметрии, а не исчезать из наблюдаемости просто потому, что ему нужен был запасной выход.
У хорошей абстракции есть запасной выход¶
Ни одна декларативная модель репозиториев не покрывает каждый запрос элегантно. Динамические фильтры поиска, необычные API драйверов, специфичные для базы данных функции и специализированная потоковая передача могут требовать ручного кода.
Kora позволяет коду репозитория опускаться до контроля уровня соединения, когда необходимо, не отказываясь от инфраструктуры транзакций и телеметрии фреймворка.
Этот многослойный путь выхода важен: сначала сгенерированный запрос, пользовательский маппер при необходимости, ручной запрос при необходимости, прямое поведение драйвера там, где это действительно необходимо.
Фреймворк без запасного выхода в конце концов становится фреймворком, с которым разработчики борются.
Нативные запросы — не режим отказа в Kora¶
В проектах ORM команды иногда говорят, что им пришлось откатиться к нативному SQL. Эта формулировка раскрывает иерархию абстракций: SQL находится ниже предпочтительной модели.
В Kora SQL — предпочтительная модель для JDBC-репозиториев. Использование специфичного для PostgreSQL синтаксиса не является выходом из фреймворка. Это намеренное использование базы данных.
Эта культурная разница влияет на то, как команды проектируют и оптимизируют персистентность.
Результаты репозиториев — обычные значения¶
Результат репозитория может быть записью, классом данных, скаляром, списком, nullable значением или проекцией, специфичной для запроса. Он не несёт скрытого жизненного цикла персистентности.
Это делает значения легче передаваемыми через слои приложения и безопаснее в использовании в конкурентной обработке запросов. Нет соблазна хранить управляемую сущность в кеше и позже обнаружить, что она отсоединена или привязана к другой сессии.
Обычные значения естественно вписываются в архитектуры «виртуальный поток на задачу».
Неизменяемые модели подходят особенно хорошо¶
Java-записи и Kotlin-классы данных — естественные проекции базы данных. Сгенерированный маппер конструирует их один раз. Они могут перемещаться по приложению без колбэков персистентности или скрытого отслеживания мутаций.
Это поощряет ориентированный на значения стиль: результат запроса, неизменяемое значение, бизнес-вычисление.
Для конкурентных сервисов меньше скрытых мутаций часто делает код легче для рассуждений.
Nullability должна следовать запросу, а не канонической сущности¶
Столбец таблицы может быть не-null, но внешнее соединение может сделать проекцию nullable. Таблица может иметь nullable-столбец, который один запрос фильтрует до не-null.
Репозитории, ориентированные на проекции, позволяют типу результата моделировать фактические семантики null запроса, а не механически копировать одно определение сущности повсюду.
Модель nullability Kotlin особенно полезна здесь.
Несколько баз данных остаются явными в графе приложения¶
Если сервис общается с несколькими базами данных, Kora может различать компоненты баз данных и связывать репозитории с корректной инфраструктурой соединений через граф на этапе компиляции.
Топология остаётся видимой. Нет требования неявного первичного контекста персистентности, который каждый репозиторий молча использует.
Это важно в системах, где владение базой данных и маршрутизация — архитектурные задачи.
Доступ к базе данных — не отдельная вселенная фреймворка¶
Некоторые стеки имеют один контейнер для зависимостей приложения и другую концептуальную подсистему для объектов, управляемых персистентностью.
Kora держит репозитории в том же графе компонентов, что и контроллеры, сервисы, клиенты, конфигурацию и телеметрию.
База данных особенная, потому что базы данных особенные, а не потому что фреймворк создаёт дополнительную вселенную управления объектами вокруг них.
Меньше контейнеров времени выполнения означает меньше ментальных моделей¶
Разработчику не нужно одновременно рассуждать о состоянии внедрения зависимостей, состоянии сессии персистентности, прокси репозиториев времени выполнения и жизненном цикле сущностей, чтобы понять базовый запрос.
Репозиторий — сгенерированный компонент с явными зависимостями.
Это сокращение ментальных моделей — одна из более широких целей дизайна Kora.
ORM могут быть высокопроизводительными¶
Ничто из этого не означает, что ORM по своей природе медленный. Хорошо спроектированное приложение Hibernate с подходящими проекциями, планами выборки, пакетированием, кешированием, индексами и знанием SQL может работать чрезвычайно хорошо.
Аргумент касается сложности на возможность, а не сырой морали бенчмарков.
Если команде нужны возможности ORM и она знает, как ими управлять, стоимость абстракции может быть полностью оправдана.
Большинство проблем ORM управляемы с экспертизой¶
N+1 можно решить. Ленивую загрузку можно контролировать. Планы выборки можно сделать явными. Кеши можно настраивать. Сессии можно ограничивать. SQL можно логировать и профилировать.
Провокационный вопрос просто в том, должны ли большинство сервисов требовать эту экспертизу в первую очередь, когда их потребности в персистентности уже.
Ответ Kora часто «нет».
Меньшая модель может быть лучшим выбором по умолчанию¶
Новый сервис Kora может начаться с типизированного репозитория, явного запроса, сгенерированного маппера, явной транзакции, JDBC и виртуальных потоков.
Если более богатая семантика жизненного цикла сущностей позже станет явно ценной, команда может намеренно оценить ORM.
Начало с меньшей модели снижает начальную сложность и делает добавление более мощных абстракций осознанным выбором, а не унаследованной конвенцией.
Лучшая архитектура часто та, где меньше скрытых машин состояний¶
Бэкенд уже содержит много машин состояний: HTTP-соединения, транзакции, блокировки, повторы, circuit breaker'ы, смещения сообщений, бизнес- рабочие процессы.
Сессия персистентности добавляет состояние управляемый/отсоединённый, отслеживание изменений, правила сброса и прокси. Реактивный конвейер добавляет состояние подписки, планировщика, контекста и противодавления.
Оба могут быть оправданы. Ни одно не следует добавлять небрежно.
Путь по умолчанию Kora нацелен на меньшее количество скрытых машин состояний: вызвать метод, выполнить запрос, вернуть значение; виртуальный поток блокируется, JVM его паркует, возобновление когда готов.
Эта простота стратегически ценна.
Современный стек меняет исторический выбор по умолчанию¶
Двадцать лет назад компромиссы выглядели иначе. Сырой JDBC был чрезвычайно многословным. Платформенные потоки были дороги при высокой конкурентности. Рефлексия времени выполнения была распространена.
Сегодня Kora может сочетать генерацию на этапе компиляции, записи и классы данных, сгенерированные репозитории, сгенерированный JSON, виртуальные потоки, современные JDBC- драйверы и быстрые интеграционные тесты.
Это меняет то, что может означать «простой доступ к базе данных».
Разумно пересматривать значения по умолчанию, созданные под более старыми ограничениями платформы.
Правильный вопрос для нового сервиса¶
Для нового сервиса Kora спросите: нужна ли нам семантика объектной персистентности или нам нужен удобный доступ к базе данных?
Если ответ — удобный доступ к базе данных, сгенерированные репозитории — обычно меньшее решение.
Если ответ действительно включает управляемый жизненный цикл агрегатов, навигацию по связям, обширное отслеживание изменений, семантику идентичности и поведение сессии персистентности, ORM может быть уместен.
Не вводите сессию персистентности лишь для избежания написания SELECT. Kora уже делает SELECT дешёвым.
Практичный чек-лист решений¶
Сервису, вероятно, не нужен полный ORM, если запросы уже важны для дизайна приложения, модели чтения различаются по случаю использования, записи — явные команды или условные обновления, транзакции малы и продуманны, специфичные для базы данных функции SQL важны, ленивая навигация по связям не центральна, неизменяемые значения желательны, JDBC удовлетворяет потребности базы данных и виртуальные потоки дают требуемую модель конкурентности.
Сервис может выиграть от ORM, если богатые графы агрегатов центральны, единица работы и отслеживание изменений материально упрощают код, навигация по связям действительно полезна или организация уже имеет глубокую экспертизу ORM и конвенции, которые перевешивают дополнительную сложность модели.
Это не моральный выбор. Это вопрос архитектурного соответствия.
Путь запроса Kora с JDBC и виртуальными потоками¶
Жизненный цикл запроса прямолинеен:
socket
↓
сетевая обработка Undertow
↓
обработчик приложения Kora
↓
виртуальный поток
↓
сервис
↓
сгенерированный репозиторий
↓
пул соединений JDBC
↓
база данных
Когда JDBC ждёт сетевого ввода-вывода, виртуальный поток может парковаться. Носитель может выполнять другую работу. Когда база данных отвечает, виртуальный поток возобновляется и продолжает обычный стек вызовов.
Это модель программирования, которую Kora хочет видеть у разработчиков.
Где на самом деле живёт предел конкурентности¶
Несколько ресурсов имеют разные роли. HTTP-сервер может поддерживать много соединений. Виртуальные потоки могут дёшево представлять много конкурентных запросов. Потоки-носители выполняют исполняемые виртуальные потоки. Пул JDBC ограничивает конкурентное использование базы данных. База данных ограничивает фактическое выполнение запросов.
Полезная иерархия: много запросов, много виртуальных потоков, ограниченные соединения базы данных, конечная ёмкость выполнения базы данных.
Понимание этой иерархии полезнее, чем просто сказать, что виртуальные потоки масштабируются.
Почему реактивный драйвер может не улучшить сервис, ограниченный пулом¶
Предположим, база данных может продуктивно обрабатывать только определённое число конкурентных запросов. Реактивный драйвер может эффективно представлять много ожидающих операций. Виртуальные потоки также могут эффективно представлять много ожидающих операций.
Ни один не меняет полезную ёмкость базы данных.
Если основной причиной принятия реактивного подхода была стоимость потоков, виртуальные потоки убирают большую часть этого преимущества. Оставшееся сравнение должно касаться потоковой передачи, противодавления, возможностей драйвера, композиции и операционного поведения.
Это более честная основа для решений.
Синхронный код всё ещё может использовать bulkhead'ы¶
Сервис с виртуальными потоками не должен позволять каждому входящему запросу бесконечно стоять в очереди за дорогой операцией базы данных.
Bulkhead'ы, семафоры, пределы пулов, таймауты и сброс нагрузки могут ограничивать конкретную работу, даже когда глобальная конкурентность запросов высока.
Реактивная архитектура не имеет монополии на ограниченную конкурентность. Механизм просто другой.
Изоляция транзакций всё ещё важна ровно как раньше¶
Виртуальные потоки не меняют READ COMMITTED, REPEATABLE READ, SERIALIZABLE, поведение фантомов, write skew или взаимоблокировки.
Явный подход Kora к JDBC может на самом деле облегчить запоминание того, что корректность транзакций принадлежит базе данных. Модель программирования изменилась. Теория баз данных — нет.
Таймауты пула соединений становятся частью SLO запроса¶
Если запрос ждёт сотни миллисекунд соединения перед выполнением быстрого запроса, задержка пользователя уже плоха.
Виртуальный поток будет ждать эффективно, что означает, что приложение может выглядеть здоровым с точки зрения потоков, в то время как очередь уже нарушает SLO.
Метрики должны отделять получение пула от выполнения запроса.
Дешёвая парковка не должна скрывать перегрузку.
Виртуальные потоки могут дольше скрывать перегрузку¶
Сервис с платформенными потоками может видимо исчерпать пул воркеров во время перегрузки. Сервис с виртуальными потоками может представлять гораздо больше запросов в очереди, прежде чем количество потоков станет очевидным сбоем.
Это хорошо, когда нагрузка транзиентна. Это опасно, если поощряет безграничные очереди.
Системам нужны явные индикаторы перегрузки: ожидание пула, задержка, память, скорость таймаутов запросов, насыщение базы данных.
Простоту Kora следует сочетать с дисциплинированными операционными бюджетами.
N+1 с виртуальными потоками всё ещё N+1¶
Запуск многих виртуальных потоков может снизить задержку по настенным часам для независимых вызовов, но не уменьшает количество запросов или работу базы данных.
Если один запрос разворачивает сотню запросов к базе данных, база данных всё ещё видит сотню операций. По многим запросам это может перегрузить пул.
Правильная оптимизация часто — один запрос на основе множеств.
Виртуальные потоки делают конкурентность дешёвой. Они не делают неэффективные паттерны доступа эффективными.
Лучшая операция базы данных часто та, которую вы не выполняете¶
Явные репозитории делают естественной оптимизацию количества запросов. Вместо «загрузи сущность, затем лениво обратись к трём связям» спроектируйте одну проекцию. Вместо «проверь, затем обнови» используйте условное обновление. Вместо вставки одной строки за раз пакетируйте. Вместо запроса каждого элемента используйте соединение, стратегию IN или другой нативный для базы данных приём.
Эти оптимизации часто затмевают накладные расходы фреймворка.
Модель доступа к данным Kora держит их видимыми.
ORM плюс виртуальные потоки тоже возможны¶
Виртуальные потоки не эксклюзивны для репозиториев Kora. Hibernate и другие приложения на основе ORM также могут выигрывать от них, с учётом поведения библиотеки и пула соединений.
Отличительный выбор Kora — сочетать два независимых упрощения: никакой сессии персистентности, если не нужно, и никакого реактивного API базы данных, если не нужно.
Вместе они создают особенно прозрачный стек.
Почему выбор Kora сильнее, чем просто использование JDBC-хелпера¶
Java годами имеет библиотеки-хелперы JDBC. Kora добавляет общефреймворковую модель на этапе компиляции: репозитории генерируются, маппинги генерируются, DI на этапе компиляции, HTTP-адаптеры генерируются, контракты OpenAPI могут генерировать границы, а виртуальные потоки поддерживают синхронный поток сервиса.
Значение не в одном API-хелпере. Оно в согласованности архитектуры.
Большая часть структурной адаптации происходит до времени выполнения. Код приложения остаётся близко к лежащим в основе технологиям.
Модель репозиториев дополняет разработку OpenAPI-First¶
На HTTP-границе контракт OpenAPI может генерировать типизированный серверный и клиентский код. На границе базы данных SQL плюс сигнатура репозитория генерирует JDBC-адаптер.
Сервис находится между двумя явными контрактами.
Архитектура становится: внешний протокол явный, бизнес-логика явная, операция базы данных явная, склейка фреймворка сгенерирована.
Очень мало важного поведения скрыто в конвенции времени выполнения.
Это отличительная сильная сторона дизайна Kora.
Тонкие границы уменьшают семантический перевод¶
Глубокий стек может выглядеть как HTTP-абстракция, сервисная абстракция, ORM- абстракция, драйвер, база данных.
Kora нацелен на сгенерированный HTTP-адаптер, код приложения, сгенерированный адаптер репозитория, JDBC, базу данных.
Каждая граница относительно тонкая. Путь запрос-к-SQL легче проследить.
Это ценно для производительности, отладки, адаптации и разработки с помощью нейро-инструментов.
Нейро-агенты выигрывают от явного доступа к данным¶
Агент кодирования может проверять метод репозитория, SQL, модель, миграцию и сгенерированную реализацию. Ему не нужно реконструировать скрытое состояние контекста персистентности, правила ленивых связей, поведение прокси или семантику сброса, прежде чем понять запрос.
Если маппер отсутствует, компиляция может завершиться ошибкой. Если SQL неверен против схемы, интеграционный тест может завершиться ошибкой.
Это создаёт тесный и относительно детерминированный цикл обратной связи.
SQL — общий технический язык для людей и агентов¶
Запрос сообщает намерение базы данных напрямую. DBA может его проверить. Бэкенд- инженер может его проверить. нейро-агент может его проверить. База данных может объяснить свой план для него.
Этот общий язык уменьшает семантический перевод.
Сгенерированная склейка позволяет командам сохранять этот общий язык без оплаты ручного JDBC-шаблонного кода.
Компилятор становится первым рецензентом¶
Если структура репозитория и маппинг не сходятся, Kora может завершиться ошибкой во время обработки аннотаций или KSP. Если в графе приложения отсутствует требуемый маппер или компонент базы данных, компиляция может завершиться ошибкой.
Интеграционный тест остаётся необходимым для фактического SQL и живого поведения схемы, но многие ошибки на стороне Java никогда не достигают времени выполнения.
Это ценно и для человеческой, и для автоматизированной разработки.
Kora действительно от чего-то отказывается¶
С репозиториями Kora разработчики пишут SQL или CQL. Компилятор Java не может полностью доказать произвольный SQL против производственной схемы. Богатая семантика контекста персистентности отсутствует. Автоматическая проверка изменений отсутствует. Автоматическая навигация по связям отсутствует. Сложные динамические запросы могут требовать ручного кода или другого инструмента.
Это реальные стоимости.
Тезис в том, что многие сервисы лучше принимают их, чем платят за полную модель ORM, которую едва используют.
Что Kora получает¶
Взамен команды получают явные запросы, предсказуемое количество запросов, обычные неизменяемые значения, проекции, специфичные для запроса, прямые функции базы данных, сгенерированный шаблонный код JDBC и Cassandra, маппинг на этапе компиляции, явные транзакции, синхронные API, конкурентность, дружелюбную к виртуальным потокам, и гораздо меньший семантический разрыв с базой данных.
Для целевых рабочих нагрузок Kora это убедительный пакет.
ORM всё ещё может быть правильным инструментом¶
Если приложение действительно вращается вокруг персистентных графов агрегатов, автоматического поведения единицы работы, отслеживания изменений, жизненного цикла связей и кеширования на уровне провайдера, ORM может быть более продуктивной моделью.
Правильный вывод — не убрать ORM из набора инструментов.
Это перестать рассматривать ORM как обязательную инфраструктуру для каждого сервиса, использующего реляционную базу данных.
Выбор по умолчанию должен соответствовать общей рабочей нагрузке¶
Kora нацелен на облачные бэкенд-сервисы, где запуск, эффективность времени выполнения, явные границы и предсказуемое поведение базы данных важны.
Для этой рабочей нагрузки сгенерированные JDBC-репозитории — связный выбор по умолчанию. ORM можно вводить, когда его более богатая семантика действительно полезна, а не унаследована по конвенции.
Это здоровое обращение традиционного Java-выбора по умолчанию для персистентности.
Заключение¶
Kora не нужен ORM для большинства сервисов, потому что он атакует две исторические причины, по которым разработчики тянулись к нему, с другой стороны.
Первая проблема была эргономикой JDBC. Сырой JDBC требует слишком много повторяющегося кода: операторы, индексы параметров, циклы по наборам результатов, маппинг, обработку ресурсов, телеметрию и транзакционную обвязку. Kora решает это с помощью сгенерированных на этапе компиляции репозиториев и мапперов. Разработчик пишет операцию базы данных и типизированный метод; фреймворк пишет склейку.
Вторая проблема была блокирующей конкурентностью. Традиционный синхронный JDBC привязывал каждый ожидающий запрос к тяжёлому платформенному потоку, что делало очень высокую конкурентность ввода-вывода дорогой. Реактивные API баз данных предлагали убедительную альтернативу, мультиплексируя работу по небольшим пулам потоков. Виртуальные потоки меняют этот компромисс. Сервис Kora может сохранить прямолинейный синхронный вызов репозитория, в то время как JVM паркует ожидающий виртуальный поток и переиспользует его носитель для другой работы.
Получившаяся архитектура проста:
простой синхронный репозиторий
+
JDBC
+
виртуальные потоки
=
высокая конкурентность без реактивного API базы данных
Эта простота не означает, что база данных становится простой. Пул соединений JDBC остаётся конечным. База данных остаётся конечной. Блокировки, транзакции, планы запросов, индексы, таймауты и закон Литтла всё ещё важны. Виртуальные потоки делают ожидание дешёвым; они не делают конкурентность безграничной. На самом деле, потому что приложение может представлять так много конкурентных запросов дёшево, продуманные пределы конкурентности базы данных становятся ещё более важными.
Аргумент также не требует объявлять ORM плохой технологией. ORM решают реальные проблемы. Контексты персистентности, карты идентичности, отслеживание изменений, каскады, навигация по связям, автоматическое управление жизненным циклом и кеширование могут быть отличными инструментами для приложений, чья естественная модель — персистентный граф объектов.
Провокационный момент в том, что это не естественная модель каждого сервиса.
Многие бэкенд-сервисы ориентированы на операции. Они получают запрос, читают несколько строк, применяют политику, обновляют состояние, пишут событие outbox и возвращают результат. Их чтения часто являются проекциями, специфичными для запроса. Их записи часто являются условными обновлениями. Их производительность зависит от явного SQL, индексов, ёмкости пула и дизайна транзакций. Их домен может вообще не выигрывать от управляемой идентичности сущностей.
Для этих сервисов ORM может ввести вторую модель времени выполнения между кодом приложения и базой данных: сессии, управляемое и отсоединённое состояние, грязные снимки, ленивые прокси, правила сброса, планы выборки и кеши. Опытные команды могут хорошо управлять этими концепциями, но они остаются концепциями, которые должен выучить каждый разработчик и которые каждому реагирующему на инцидент может в конечном счёте понадобиться понять.
Kora выбирает меньшую модель.
SQL остаётся SQL. База данных сохраняет свой нативный язык, планировщик запросов, модель блокировок и специфичные для вендора возможности. Сервис PostgreSQL может использовать RETURNING, ON CONFLICT, JSONB, массивы, CTE или SKIP LOCKED, не дожидаясь, пока другая абстракция их раскроет. Сервис Cassandra пишет CQL и уважает моделирование данных Cassandra, а не притворяется, что у него есть реляционные транзакции.
Java остаётся Java. Результаты репозиториев могут быть неизменяемыми записями или Kotlin- классами данных. Проекции, специфичные для запроса, могут представлять ровно те поля, которые нужны случаю использования. Значения не несут скрытого жизненного цикла персистентности. Изменение объекта не молча планирует обновление базы данных. Операция базы данных происходит, потому что код вызывает операцию базы данных.
Kora генерирует склейку. Привязка параметров, маппинг строк, пакетная обвязка, сгенерированные идентификаторы, использование соединений, телеметрия и реализации репозиториев выводятся на этапе компиляции. Макросы запросов убирают повторяющееся сопровождение списков столбцов, не превращая SQL в DSL фреймворка. Транзакции остаются явными. Пользовательские мапперы и ручные запросы предоставляют запасные выходы, когда требуются нативные функции базы данных.
Затем виртуальные потоки завершают картину. Они позволяют Kora 2 использовать эту прямолинейную синхронную модель доступа к данным при высокой конкурентности ввода-вывода, не возрождая старую стоимость «платформенный поток на запрос». Исходный код приложения может оставаться обычным, в то время как JVM обрабатывает дешёвую парковку и планирование внизу.
Вот почему «Kora не нужен ORM» — на самом деле не анти-ORM лозунг. Это заявление об экономике абстракций.
Если сервису нужна объектная персистентность, используйте инструмент, спроектированный для объектной персистентности.
Если сервису нужно выполнять хорошо спроектированные операции базы данных и преобразовывать их в типизированные значения приложения, добавление модели объектной персистентности может быть ненужным.
Для большого класса современных бэкенд-сервисов меньшего стека достаточно:
явный запрос
+
типизированный репозиторий
+
сгенерированная склейка
+
явная транзакция
+
JDBC
+
виртуальные потоки
И когда меньшего стека достаточно, меньше скрытых запросов, меньше состояний жизненного цикла, меньше кешей, меньше прокси, меньше слоёв времени выполнения и меньше абстракций конкурентности — это не отсутствующие функции.
Это сложность, за которую сервису никогда не приходилось платить.