Объектные запросы и ORM-операции#
Объектные запросы и ORM-операции — работают через ORM: типобезопасны, интегрированы с сессией, видят изменения текущей сессии. Подходят для бизнес-логики и стандартных выборок.
OQuery;TxIndex;ElExpOQuery;SEntityBaseApi.load();ChildApi.byParent();ChildBaseApi.refreshByParent();Btk_QueryPkg.largeInQuery();refresh(entity);flush.
Поведение при работе с кешем и БД#
Объектные запросы взаимодействуют с кешем и базой данных по-разному в зависимости от используемого метода:
Метод |
Проверка кеша перед обращением к БД |
Всегда обращается к БД |
Примечание |
|---|---|---|---|
|
да |
нет |
Возвращает объект из кеша, если он уже загружен в сессию |
|
да |
нет |
Использует кеш коллекций, если данные были предзагружены |
|
да |
нет |
Возвращает данные из транзакционного индекса, при отсутствии — запрашивает из БД |
|
частично |
да* |
Условие |
|
нет |
да |
Принудительно перечитывают данные из БД, инвалидируя кеш |
|
нет |
да |
Обновляет данные индекса напрямую из БД |
* OQuery выполняет SQL-запрос к БД, но при маппинге результатов учитывает несохранённые изменения объектов в текущей сессии.
Пример запроса:
new OQuery(Bs_GoodsAta.Type) {
where(t.sSystemName === spMnemoCode)
}
OQuery предоставляет подмножество JPQL
и используется для выбора данных по классу.
ORM#
В Global ERP для ORM-запросов используется EclipseLink. Это технология, позволяющая работать с БД, используя объектно-ориентированный код вместо написания SQL-запросов. Она выступает прослойкой, преобразуя данные между таблицами БД и объектами в языках программирования, что упрощает разработку, повышает читаемость кода и защищает от SQL-инъекций.
Кеширование в ORM#
В ORM используются два основных уровня кеширования: кеш текущей сессии и shared кеш.
Кеширование — хранение уже полученных из БД данных в памяти, чтобы повторно их не запрашивать.
Кеширование необходимо для:
Снижения нагрузки на БД: меньше реальных SELECT/UPDATE.
Ускорения ответов: данные берутся из памяти, а не диска/сети.
Стабилизации производительности: часто используемые справочники, настройки, профили пользователей читаются мгновенно.
Какую функцию выполняет при работе с БД
Чтение: ORM сначала смотрит в кеш; если запись есть — отдает ее без запроса в БД.
Версионность и изоляция: каждая сессия хранит свою версию объекта в кеше, что предотвращает конфликты при параллельном доступе.
Изменение/Удаление: ORM отправляет изменения в БД и одновременно сбрасывает кеш, чтобы не вернуть устаревшие данные.
Выборки: для сложных SELECT возможен кеш запросов (в кеше живут готовые результаты выборок).
Внимание
ORM гарантирует, что данные в кеше не устаревают: перед чтением ORM проверяет актуальность записи в кеше, а при любом изменении или удалении сущности кеш инвалидируется. Если версия в кеше вызывает сомнения, данные загружаются напрямую из базы.
Методы, использующие кеш#
Метод |
Какой кеш использует |
Примечание |
|---|---|---|
|
кеш текущей сессии и shared кеш |
При загрузке по идентификатору может вернуть уже загруженный объект без повторного обращения к БД. |
|
кеш текущей сессии |
Может читать коллекцию из памяти, если она уже была загружена ранее или предзагружена через |
|
кеш текущей сессии и shared кеш |
При выполнении учитывает изменения текущей сессии. Загруженные объекты могут использовать shared кеш, но сам запрос не всегда обходится без обращения к БД. |
|
shared кеш |
Может использовать |
|
shared кеш |
При повторном выполнении того же запроса с теми же условиями результат может быть получен из кеша без повторного SQL. |
|
кеш текущей сессии |
Возвращает согласованные данные с учетом изменений текущей сессии; при наличии данных в индексе может не обращаться к БД. |
|
кеш текущей сессии |
Использует закешированный набор ключей индекса в пределах текущей сессии. |
|
кеш текущей сессии |
Выполняет загрузку связанных коллекций в кеш; после этого они могут читаться без дополнительного обращения к БД. |
|
кеш текущей сессии |
Загружает указанные коллекции в кеш для последующего чтения без дополнительного запроса. |
Методы load, OQuery, byParent, TxIndex и связанные с ними механизмы используют общий объектный кеш текущей сессии. Для shared-сущностей дополнительно может использоваться shared кеш приложения. Это позволяет повторно использовать уже загруженные Rop между разными способами доступа к данным.
Ограничения кеша и очистка#
Кеш текущей сессии растёт по мере загрузки и изменения объектов в рамках транзакции. При длительных или объёмных операциях это может приводить к повышенному потреблению памяти и замедлению работы ORM.
Когда это важно
при обработке больших выборок;
при длительных транзакциях;
при массовом создании или изменении объектов;
при накоплении большого количества данных в кеше без очистки.
Как контролировать размер кеша
обрабатывать данные частями;
не загружать в память больше данных, чем требуется;
для массовых операций использовать реляционные запросы, если не требуется ORM;
при длительных операциях своевременно очищать кеш текущей сессии.
Очистка кеша
flush— синхронизирует изменения с базой данных;commitWork— завершает транзакцию и освобождает кеш текущей сессии;flush(true)— может использоваться для принудительной синхронизации и контроля состояния кеша в длительных операциях.
Подробнее о работе commitWork, flush и очистке кеша см. в принципах работы commit и flush.
Пакетные операции (bulk)#
Оптимизации массовой обработки данных. chunkedQuery позволяет безопасно загружать и обрабатывать большие объемы объектов порциями, контролируя потребление памяти и производительность системы.
Когда использовать:
когда стандартные средства фильтрации недостаточны;
требуется сложная выборка с соединениями таблиц.
refresh(entity)#
Принудительно перечитывает состояние сущности из базы данных.
Метод обновляет объект по актуальному состоянию в БД и отменяет локальные несохраненные изменения, внесенные в него в текущем контексте.
Когда использовать:
если запись могла быть изменена другим процессом, приложением или пользователем;
если нужно сбросить локальные несохраненные изменения и вернуть объект к состоянию из БД;
если перед дальнейшей обработкой требуется получить актуальное состояние сущности без завершения текущего контекста работы.
Пример:
override protected def onRefresh: Recs = {
val rop = thisApi().refresh(getVar(CardRep.IdItemSharp).asNLong)
rop.get(_.gidRec).option.foreach { gidv =>
val vApi = Btk_ClassApi().getSApiByIdClass(gidv.parseIdClass())
vApi.refresh(gidv.parseIdNLong())
}
rop
}
Примечание
load - загружает запись по идентификатору и использует кеш текущей сессии.
refresh(entity) - перечитывает уже загруженную запись из БД и обновляет её в кеше текущей сессии.
flush#
Принудительно синхронизирует изменения текущего контекста с базой данных.
При вызове метода ORM отправляет в БД накопленные операции INSERT, UPDATE и DELETE, но не завершает транзакцию.
Когда использовать:
если нужно гарантировать, что изменения уже отправлены в базу данных до выполнения следующих действий в рамках той же транзакции;
если после изменения объектов выполняется реляционный запрос, который должен видеть актуальные данные;
если требуется получить согласованное состояние перед объектным или реляционным запросом;
если нужно явно отделить момент отправки изменений в БД от момента завершения транзакции.
Внимание
flush() синхронизирует накопленные изменения с базой данных, но не завершает транзакцию. После flush() изменения остаются частью текущей транзакции и могут быть откатаны, если позже возникнет ошибка.
commit() завершает транзакцию. Перед завершением транзакции commit() выполняет flush(), то есть сначала отправляет изменения в базу данных, а затем фиксирует их без возможности отката.
Пример:
def regStates(idpClass: NLong): Unit = {
Btk_ClassStateApi().register(
idpMasterClass = idpClass,
spSystemName = "Create",
spCaption = "Оформляется",
bpStartState = 1.nn,
npOrer = 100.nn)
session.flush() // чтобы начальное состояние уже было отправлено в БД
Btk_ClassStateApi().register(
idpMasterClass = idpClass,
spSystemName = "InWork",
spCaption = "Передан в работу",
bpStartState = 0.nn,
npOrer = 200.nn)
session.flush()
}
OQuery#
Основной механизм выполнения объектных запросов.
Позволяет:
выбирать объекты по условиям;
сортировать результат;
управлять загрузкой связанных объектов;
Особенности работы с кешем
OQueryвыполняет выборку объектов класса и обращается к базе данных;при выполнении запроса учитываются изменения текущей сессии, поэтому результат может содержать данные, еще не синхронизированные с БД;
в кеш помещаются загруженные объекты, а не «результат запроса» как самостоятельная сущность;
связанные коллекции не считаются автоматически загруженными только из-за выполнения
OQuery: их загрузка и кеширование зависят от способа обращения к ним;для массовой предварительной загрузки коллекций используются
batchAllиbatchIn; после такой загрузки коллекции могут читаться без дополнительного обращения к БД, например черезbyParent.
Когда использовать:
стандартные выборки объектов;
бизнес-логика;
работа с ORM-сущностями;
сценарии, где важно учитывать изменения текущей сессии.
Пример:
new OQuery(Bs_GoodsAta.Type) {
where(t.sSystemName === spMnemoCode)
}
Внимание
Создание экземпляра OQuery не запускает обращение к базе данных. Запрос выполняется только при первом обращении к результатам — например, при вызове .toVector, .toList, .head или итерации в цикле for.
Для предварительной загрузки данных и явного запуска выполнения запроса используйте терминальные операции:
val results = new OQuery(Bs_GoodsAta.Type) {
where(t.sSystemName === spMnemoCode)
}.toVector // запускает выполнение запроса и загружает результаты в память
Без .toVector или аналогичного вызова запрос не будет выполнен, и изменения не попадут в кеш сессии.
where#
Добавляет условие фильтрации.
Пример:
new OQuery(Entity.Type) {
where(t.status === "ACTIVE")
}
SQL аналог:
WHERE status = 'ACTIVE'
orderBy#
Сортировка результата.
Пример:
new OQuery(Entity.Type) {
where(t.status === "ACTIVE")
orderBy(t.name)
}
batchAll#
Массовая загрузка всех коллекций объекта.
Проблема, которую решает
Избегает N запросов для получения объектов коллекций, вместо этого выполняет 1 запрос с учетом коллекций и добавляет их в объектный кеш для последующей загрузки по byParent без обращения к базе данных.
Когда использовать:
при загрузке сложных объектов;
при работе с большим количеством связанных данных.
Пример:
new OQuery(Entity.Type) {
batchAll()
}
batchIn#
Массовая загрузка конкретных коллекций.
Когда использовать:
Если нужно загрузить только часть связей.
Пример:
new OQuery(Order.Type) {
batchIn(t.items)
}
forUpdate#
Блокирует выбранные строки.
Когда использовать:
перед изменением записи;
при конкурентном доступе.
SQL аналог
SELECT ... FOR UPDATE
Пример:
new OQuery(Entity.Type) {
where(t.id === id)
forUpdate()
}
forUpdateNoWait#
Блокирует записи без ожидания.
Если запись уже заблокирована — возникает ошибка.
Когда использовать:
в сценариях высокой конкуренции;
когда ожидание блокировки недопустимо.
unique#
Указывает, что запрос должен вернуть одну запись.
Используется для поиска по уникальному атрибуту и позволяет задействовать cache-index, если он настроен в ORM-описании сущности с разделяемым режимом кеширования (Shared).
cache-index задается в ORM-описании класса по полям, которые используются в условии where. Например, для атрибута мнемокода описание может выглядеть так:
<`cache-index`>
<column-name>SSYSTEMNAME</column-name>
</`cache-index`>
В этом случае запрос по соответствующему полю может использовать кешированный доступ к сущности:
new OQuery(entityAta.Type) {
unique()
where(t.sSystemName *=== spMnemoCode)
}
Когда использовать:
при поиске по уникальному атрибуту;
для
lookupсправочников;когда в ORM-описании настроен
cache-indexпо полю, участвующему в условии поиска.
tryCacheQueryResults#
Включает кеширование результата объектного запроса.
Кеширование запросов работает только для классов с разделяемым режимом кеширования (Shared). Если класс настроен для сохранения в разделяемом кеше, результат запроса может быть сохранен и использован при повторном выполнении того же запроса.
Если транзакция находится в режиме редактирования разделяемых объектов, кеширование результата не выполняется.
Режим редактирования разделяемых объектов включается:
session.setDefaultUOWEditType(RWSharedUOWET)
или
Btk_Pkg().setRWSharedUOWEditType()
В этом режиме shared-сущности загружаются из БД, а не из кеша, поэтому использование tryCacheQueryResults() не даёт эффекта.
Как используется кеш
при первом выполнении запроса выполняется обращение к базе данных;
кешируется соответствие между условиями запроса и найденными объектами;
при повторном выполнении того же запроса с теми же условиями результат может быть получен из кеша без повторного обращения к БД;
если кеш был инвалидирован, запрос снова будет выполнен к базе данных.
Когда использовать:
для часто повторяющихся одинаковых запросов;
для чтения справочников, настроек и других редко изменяемых данных;
когда важна скорость повторного чтения при одинаковых параметрах запроса.
Пример:
new OQuery(entityAta.Type) {
tryCacheQueryResults()
where(t.sSystemName === spMnemoCode)
}
Операции выборки для загрузки и обновления данных#
Операции выборки — предопределённые операции выборки, через которые в интерфейсной логике обычно выполняется загрузка и обновление данных.
Эти операции не являются отдельным механизмом доступа к БД, а используют объектные или реляционные запросы.
onRefresh— загружает весь набор данных выборки;onRefreshItem— обновляет одну текущую запись;onRefreshExtвыполняет дополнительный запрос для загрузки заголовков ссылочных объектов и используется вместе с объектными запросами вonRefresh/onRefreshItem;mergeItems.
Примечание
Подробное описание операций выборки, включая onRefresh, onRefreshItem, onRefreshExt, а также других предопределённых операций, см. в документации по выборкам.
mergeItems#
Обновляет существующие записи выборки и добавляет отсутствующие из переданного DataStorePacketSource.
При объектном запросе mergeItems обновляет или добавляет в выборку переданные записи.
Как работает
если запись уже есть в выборке, её данные обновляются;
если записи нет, она добавляется в выборку;
новые записи добавляются после текущей записи, если
isHighCapacityEnabled()выключен;новые записи добавляются в конец списка, если
isHighCapacityEnabled()включен.
Параметры
dspSource— источникDataStorePacket.
Когда использовать
если нужно частично обновить данные выборки без полного
onRefresh;если нужно добавить в выборку новые записи и одновременно обновить уже существующие;
если данные уже получены и их нужно встроить в текущий датасет выборки.
Пример:
selection.mergeItems(recs)
где recs — это новые или обновлённые записи.
Примечание
mergeItems не выполняет самостоятельный запрос к БД. Обычно метод используется после получения данных объектным или реляционным запросом.
Транзакционный индекс (TxIndex)#
Позволяет получить согласованный с локальными изменениями текущей сессии перечень строк по значению индексируемого атрибута.
Транзакционный индекс подгружает данные из базы данных по мере обращения к ключам индекса и учитывает изменения текущей сессии, еще не синхронизированные с БД. Благодаря этому можно получить актуальный набор строк и значения их атрибутов даже до выполнения flush.
Особенности
выполняет поиск по одному полю по условию равенства;
не поддерживает поиск по составному индексу из нескольких полей как единое условие;
возвращает результат в виде списка
Rop;использует данные из транзакционного кеша текущей сессии и при необходимости дополняет их данными из БД;
учитывает локальные изменения текущей сессии, поэтому результат остается согласованным даже до синхронизации с базой данных.
Взаимодействие нескольких индексов на один класс
Если для одного класса объявлено несколько транзакционных индексов, они используют общий объектный кеш Rop.
Это означает, что данные, подгруженные через TxIndex, load, OQuery или byParent, могут переиспользоваться друг другом без повторного создания отдельных копий объектов.
Например, если данные были предварительно загружены через queryKeys(), последующий load() того же объекта обычно использует уже имеющийся в кеше Rop, а не обязательно выполняет новый запрос к БД.
Пример использования:
lazy val idxCategory = TxIndex(Bs_GoodsAta.Type)(_.idCategory)
/** Поиск неотмененных ТМЦ по категории */
def byCategoryNotCanceled(): Iterable[ApiRop] = {
idxCategory.byKey(idvGdsCategory)
.filter(_.get(_.idStateMC) > 0.nn)
}
Если нужно несколько условий
Если требуется выполнить отбор по нескольким условиям, используйте индекс по тому полю, которое вернет наименьшее количество записей, а остальные условия применяйте дополнительно через .filter().
Использование в цикле
Если транзакционный индекс используется в цикле, рекомендуется заранее загрузить результаты в память по набору ключей, чтобы избежать повторных обращений при каждой итерации. Подробнее можно прочитать здесь.
byKey#
Получение записей по ключу индекса.
Пример:
idxParent.byKey(parentId)
refreshByKey#
Принудительно перечитывает данные по ключу индекса из базы данных.
В отличие от byKey, не использует ранее загруженное состояние как основной источник результата, а обновляет его.
Когда использовать:
если нужно получить актуальные данные по ключу после внешних изменений;
если есть сомнение, что ранее загруженное состояние индекса устарело;
если требуется явно обновить данные перед дальнейшей обработкой.
Пример:
lazy val idxCategory = TxIndex(Bs_GoodsAta.Type)(_.idCategory)
// Принудительно перечитать записи по категории из БД
val goods = idxCategory.refreshByKey(idvGdsCategory)
for (rop <- goods) {
println(rop.get(_.gid))
}
Типовой сценарий использования refreshByKey — когда данные были ранее загружены в кеш, но затем могли измениться и быть зафиксированы другой сессией. В таком случае byKey может вернуть ранее загруженное состояние, а refreshByKey позволяет инвалидировать его и перечитать актуальные данные из БД.
queryKeys#
Предварительно загружает данные по набору ключей. Используется, когда транзакционный индекс нужен в цикле или в массовой обработке и требуется избежать повторных обращений по одному ключу за раз.
Когда использовать:
перед обработкой большого набора ключей в цикле;
для снижения количества обращений к БД;
для предварительной прогрузки данных в память.
Пример:
lazy val idxCategory = TxIndex(Bs_GoodsAta.Type)(_.idCategory)
val categoryIds = Seq(cat1, cat2, cat3)
// Предварительная загрузка данных по набору ключей
idxCategory.queryKeys(categoryIds)
// Дальнейшая работа уже с прогруженными данными
for (categoryId <- categoryIds) {
val goods = idxCategory.byKey(categoryId)
println(goods.size)
}
forPartition#
Открывает секцию массового обновления транзакционного индекса. Используется в сценариях, где индекс нужно обновлять большими пачками, не перестраивая его после каждого отдельного изменения.
Когда использовать:
при массовых изменениях данных;
при пакетной обработке;
после очистки транзакционного кеша, когда индекс нужно обновить согласованно.
forPartition особенно важен в случаях, когда изменяются поля, являющиеся ключами транзакционного индекса.
Если такие изменения выполняются массово, рекомендуется оборачивать их в forPartition, чтобы индекс обновлялся согласованно и не перестраивался после каждого отдельного изменения.
Пример:
lazy val idxCategory = TxIndex(Bs_GoodsAta.Type)(_.idCategory)
idxCategory.forPartition {
for (data <- goodsToUpdate) {
// изменяется поле, по которому построен индекс
data.set(_.idCategory, newCategoryId)
}
}
load#
Метод load используется для загрузки записи по идентификатору через соответствующий API класса.
Например:
val rop = Bs_GoodsApi().load(31.nl)
val code = rop.get(_.sCode)
После вызова load возвращается объект типа rop. Для получения значения полей объекта используется метод get, которому в качестве аргумента передается функция-указатель на нужное поле.
Когда использовать:
Когда требуется загрузить запись по идентификатору через API.
Если нужно получить строго существующую запись (отсутствие записи считается ошибкой).
Используется для бизнес-логики, где важно быть уверенным, что объект есть.
Объектный запрос c помощью выражений EclipseLink#
Функционал, расширяющий использование объектных запросов, который позволяет запрашивать данные из неизвестных классов (когда заранее неизвестен тип Ata). Класс:ru.bitec.app.gtk.eclipse.query.ElExpOQuery
Когда использовать:
когда тип объекта неизвестен заранее;
динамические сценарии.
Пример:
new ElExpOQuery(api).forWhere(
_.get("sSystemName").equal("test".ns.get)
).addOrdering(
_.get("sCaption")
)
Примеры использования см. в ru.bitec.app.btk.ElExpOQueryTest
refreshByParent и byParent#
Методы refreshByParent и byParent используются для получения данных коллекции по ссылочному полю на мастер-объект.
Они доступны только у коллекций и возвращают результат в виде списка Rop, что удобно для дальнейшего использования в бизнес-логике и передачи в методы, работающие с Rop.
Особенности работы методов
поиск выполняется по ссылке на мастер-объект;
результат возвращается с учетом кеша;
refreshByParentинвалидирует ранее загруженные данные и получает актуальное состояние из базы данных;byParentсначала использует данные из памяти, если они уже были загружены, а при их отсутствии обращается к базе данных;если
byParentиспользуется в цикле, рекомендуется заранее выполнить предварительную прогрузку данных в кеш, например с помощью OQuery.
Объектный запрос большого списка#
Выполнение объектного запроса с большим списком значений IN.
Представляет собой обертку над ElExpOQuery (EclipseLink Expression Object Query) из пакета Btk_QueryPkg. Предназначена для случаев, когда список объектов для условия IN превышает 2000 элементов и СУБД не справляется с обработкой такого объема.
Ограничения
Не может использоваться внутри
OQuery.Вызывается как отдельная функция пакета
Btk_QueryPkg.
Когда использовать:
большие выборки по списку ID;
массовые операции.
in.ru.bitec.app.btk.Btk_QueryPkg#largeInQuery
смотри примеры использования: ru.bitec.app.btk.Btk_QueryPkgTest