Объектные запросы и 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 cache). Общие принципы кэширования данных, отличия разделяемого и транзакционного кэша, правила использования и сброса описаны в разделе Кэширование данных.
На этой странице описаны особенности конкретных ORM-операций: какие методы могут использовать уже загруженные данные, какие методы принудительно обращаются к БД и как операции синхронизации влияют на кеш текущей сессии.
Чтение данных в обход разделяемого кеша#
Полностью отключить разделяемый кеш (shared cache) штатным способом нельзя.
Если требуется прочитать актуальное состояние shared-сущностей из БД, сессию можно перевести в режим редактирования shared-сущностей. В этом режиме shared-сущности загружаются из БД, а не из кеша.
Для этого можно использовать один из вариантов:
session.setDefaultUOWEditType(RWSharedUOWET)
или
Btk_Pkg().setRWSharedUOWEditType()
Также актуальные данные можно получить через refresh, refreshByParent, refreshByKey или через прямой SQL-запрос.
Ограничения кеша текущей сессии#
Кеш текущей сессии растет по мере загрузки и изменения объектов в рамках операции. При длительных или объемных операциях это может приводить к повышенному потреблению памяти и замедлению работы ORM.
Общие принципы очистки транзакционного кэша описаны в разделе Кэширование данных. Подробнее о работе 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-описании сущности с разделяемым режимом кеширования.
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#
Включает кеширование результата объектного запроса.
Кеширование запросов работает только для классов с разделяемым режимом кеширования. Если класс настроен для сохранения в разделяемом кеше, результат запроса может быть сохранен и использован при повторном выполнении того же запроса.
Если транзакция находится в режиме редактирования разделяемых объектов, кеширование результата не выполняется.
Режим редактирования разделяемых объектов включается:
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.