# Урок 2. Сессии, Модель данных ## Цель курса Целью курса является познакомить слушателей с возможностями Global Framework. В рамках этого курса будет создано приложение по управлению библиотекой. ![Library](/img/library.png) ## Сессия Сессия приложения создается на поток прикладной бизнес логики и предоставляет доступ к сессии базы данных, EclipseLink кэшу, серверу приложения. А так же содержит необходимые инъекции зависимости для работы прикладной бизнес логики. Сессия приложения создается: - на каждую mdi форму открытую в приложении - на rest запрос к серверу приложения ```{note} Для ускорение rest запросов возможна настройка пула сессий ``` Для изучения смотри: [Руководство разработчика: Сессия-приложения](books/AppGuide/020_common/050_сессия_приложения.md#Сессия-приложения) ## Бизнес объект Бизнес-объект (БО) - объединение нескольких классов и их коллекций в группу для более удобного манипулирования ими при работе с кэшем и конфигурировании вспомогательных сервисов. Бизнес объект позволяет: - Массово загружать данные в транзакционный кэш \ Для бизнес объекта можно указать стратегию загрузки данных существенно уменьшающую количество запросов в базу данных. Так как запросы пойдут не по каждому объекту а по каждому классу бизнес объекта. - Настраивать права доступа \ По бизнес объекту создается административный объект на котором можно массово выдать привилегии для всех классов бизнес объекта - Управлять электронной подписью \ Можно настроить правила подписи всего бизнес объекта включая не только шапку но и все вложенные коллекции. - Настраивать интеграцию и репликацию Для изучения смотри: [Руководство разработчика: Бизнес-объект ](books/AppGuide/030_class/060_класс.md#Бизнес-объект) ## Общие сведения о классах Определяет правила хранения и обработки таблицы базы данных. Класс позволяет существенно ускорить разработку бизнес логики ориентированную на работу с данными. Программисту достаточно объявить перечень атрибутов класса чтобы за счет кодо-генерации получить набор готовых сервисов. Перечень генерируемых элементов: - *Доменная автономная бизнес логика*(`Dpi`) \ Содержит код для автономной бизнес логики - *Каркас прикладной автономной логики*(`Api`) \ `scala` класс с окончанием `Api`, в котором пишется автономная бизнес логика для работы с классом. Наследуется от `Dpi` - *Доменная интерактивная бизнес логика*(`Dvi`) - *Каркас прикладной интерактивной логики*(`Avi`) \ `scala` класс с окончанием `Avi`, в котором пишется интерактивная бизнес логика. Наследуется от `Dvi` - *Доменная разметка выборки*(`dvm.xml`) \ Содержит сгенерированную по умолчанию разметку выборки. - *Каркас прикладной декларации пользовательского интерфейса*(`Avm`) \ `xml` файл с расширением `avm.xml`, в котором пишется разметка выборки - Интеграция с `Orm` - `Pojo` объект для хранения данных в кэше - `Aro` объект интеграции pojo в фреймворк ### Типы данных В система имеет специализированный набот простых и объектных типов для удобной обработки данных. Объектные типы при необходимости интегрированны в контекст сессии что позволяет обеспечить высокую произоводительность системы за счет минимизации операций сериализации\десириализации. Основные типы данных, используемые для атрибутов класса: - Целое число - Дробное число - Строка - Дата - Ссылка (на объект заданного класса) - Ссылка на класс - Переменная ссылка (на объект произвольного класса) - Глобальный идентификатор gid - Json контейнер #### Простые типы Простыми типами являются: Число, строка, дата | Тип | postgresql | odm | Рекомендация по использованию | |-|-|-|-| | Строка фиксированной длинны | varchar | Varchar | Текст до 4000 символов | | Строка переменной длинны | text | Text | Текст до 15 мегабайт | | Число | number | Number | Целое или дробное число | | Дата | date | Date | Дата, дата и время | #### Ссылочные типы Данные классов могут быть связаны между собой. Для организации связей между классами используются специальные типы. ##### Ссылка на класс Хранит ссылку на класс. Обычно используется совместно с типом `переменная ссылка` для хранения класса, объект которого содержится в переменной ссылке. ##### Глобальный идентификатор gid Gid является уникальным идентификатором в рамках системы. Переменная ссылка на gid является альтернативой системе переменной ссылочности из двух атрибутов (ссылка на класс + переменная ссылка на объект). Для организации переменной ссылки через gid используется один атрибут. #### Json контейнер Json контейнер – это расширение объекта класса NoSQL нотацией в реляционной СУБД. Контейнер не имеет жесткой, заранее определенной схемы и основан на множестве пар «ключ‑значение». Это позволяет использовать его как динамическое расширение объекта. Для добавления новых данных в контейнер не требуется перекомпиляция кода или изменение структуры СУБД. Для создания нового класса в модуле необходимо: 1. Cоздать файл спецификации класса odm. Данный файл удобнее всего создать по шаблону в Install IntelliJ IDEA (как добавить такой шаблон в ide показано в 1вом уроке). ```{attention} Шаблон по умолчанию содержит коллекцию без имени и включенную группировку. Если в группировке нет необходимости её нужно удалить. Так же необходимо определить или удалить шаблоны для коллекций, в противном случае при генирации кода могут возникнуть ошибки. ``` 1. Определить атрибуты будущего класса и подключить коллекции если они имеются. 1. Сгенерировать код по файлу спецификации 1. Собрать проект 1. Сгенерировать таблицы 1. Добавить orm класса в файл src/main/resources/orm/all.xml ```xml org.eclipse.persistence.jpa.PersistenceProvider ... ru/bitec/app/lbr/Lbr_ClassName.orm.xml ... ``` ### Работа c провайдерами строк Провайдер строки - Rop используется для работы со строкой данных (Aro), загруженных в рабочее пространство, обеспечивая гарантию того, что при доступе к строке данная строка будет находиться в рабочем пространстве. Метод получения rop: ```scala thisApi().load(идентификатор.asNLong) ``` Примеры сеттеров в файле выборки: ```scala val rop = thisRop thisApi().setidContras(rop,getVar("super$id").asNLong) ``` Работа с rop в API: ```scala for (ropGrade <- new OQuery(entityAta.Type){ where (t.idGdsGrade === idpGdsGrade) }) { setidGdsGrade(ropGrade, None.nl) } ``` Для изучения смотри: [Руководство разработчика: Класс ](books/AppGuide/030_class/060_класс.md#Класс) ## Общие сведения о выборках Выборка определяет правило получения, отображение данных и обеспечивает взаимодействие с пользователем. Выборки содержат основную часть интерактивной бизнес логики. Выборка определяет: - Способ получения данных - Способ отображения данных пользователю - Бизнес логику обработки пользовательских действий Выборка может создаваться от класса с использованием кодо-генерации или вручную. Пользовательский интерфейс приложения является совокупностью экземпляров отображений выборок. Для изучения смотри: [Руководство разработчика: Выборка ](books/AppGuide/040_selection/080_выборка.md#Выборка) ## Взаимодействие с базой данных ### Объектные запросы Кроссплатформенные запросы, которые выполняются на уровне объектов класса. При выполнении запроса идет обращение к базе данных, за исключением случаев кеширования. Пример запроса ```scala new OQuery(Bs_GoodsAta.Type) { where(t.sSystemName === spMnemoCode) } ``` #### Методы - `where` \ Условие запроса. - `orderBy` \ Выражение для сортировки резуьтата - `batchAll` \ Массовая загрузка объектов. Возвращает строки с прогруженными записями всех коллекций этого класса. - `forUpdate` \ Выполнение запроса с блокированием вернувшихся записей - `forUpdateNoWait` \ Выполнение запроса с блокирование вернувшихся записей, без ожидания разблокирования, если записи уже заблокированы другой сессией. - `tryCacheQueryResults` \ Попытаться закешировать результат запроса. Смотри пункт «Кеширование» - `unique` \ Говорит, что запрос возвращает одну уникальную запись. Позволяет использовать cache-index’ы, указанные в orm класса #### Кеширование объектных запросов Кеширование запросов работает только для классов с разделяемым режимом кеширования(`Shared`). ##### Кэширование по полю Кеширование через cache-index’ы указанные в orm класса. Такой запрос должен возвращать одну строку и дополняется командой `unique()`. Например для атрибутов мнемокода класса в orm формируется запись: ```xml SSYSTEMNAME ``` Запрос выглядит следующим образом: ```scala new OQuery(entityAta.Type) { unique() where(t.sSystemName*=== spMnemoCode) } ``` ##### Кэширование объектных запросов Кэширование объектных запросов возможно по требованию в случаи если класс настроен для сохранения в разделяемом кэше. Чтобы включить кэширования запроса: 1. Добавьте в запрос опцию `tryCacheQueryResults()`. \ Результат такого запроса будет кэширован, если транзакция не находится в режиме редактирования разделяемых объектов. Пример запроса: ```scala new OQuery(entityAta.Type) { tryCacheQueryResults () where(t.sSystemName === spMnemoCode) } ``` #### Транзакционный индекс Позволяет получить перечень строк по значению индексируемого атрибута. Индекс подгружает данные из базы данных по мере обращения к ключам индекса, а так же отслеживает транзакционные изменения для получение согласованного набора строк. Это позволяет получить согласованный доступ к множеству строк по ключу, даже если индексируемое значение строки меняется в рамках этой транзакции. Пример объявления: ```scala lazy val idxidParent = TxIndex(Btk_GroupAta.Type)(_.idParentGroup) ``` Методы - `byKey` \ Посетитель по ключу индекса. - `refreshByKey` \ Посетитель по ключу индекса c обновлением из базы данных. - `queryKeys` \ Кеширование ключей индекса. - `forPartition` \ Открывает секцию для массового обновления индекса. Используется для прозрачного массового обновления после очистки транзакционного кэша. Секции могут быть вложенными друг в друга, в таком случае ключи суммируются. #### byParent Метод есть у классов коллекций, возвращает обходчик записей отфильтрованных по идентификатору предка ### Реляционные запросы Для обработки реляционных запросов в основном используется методы на базе [anorm](http://playframework.github.io/anorm/). Для более удобного использования в контекст бизнес логики добавлены дополнительные функции. #### ASQL Выполнение запроса на чтение Создаёт объект AnormSql для выполнения запроса к базе без модификации данных. Если в текущей сессии начата транзакция, SQL-выражение выполняется в ней. Если транзакция в сессии не начата, запрос выполнится в автономной sql-транзакции. Пример: ```scala val idTrigger = 1 ASQL"SELECT t.sCaption as sHeadline FROM Btk_JobSchedule t WHERE t.id = $idTrigger".as(nStr("sHeadline").*).headOption ``` #### ATSQL Выполнение запроса с изменением данных или блокировками Создаёт объект AnormSql с возможностью модификации данных. Если в текущей сессии начата транзакция, SQL-выражение выполняется в ней. Если в текущей сессии транзакция не начата, она будет начата. Пример: ```scala ATSQL("alter table bs_barcode add id int8;").execute() ``` #### ASelect Выполнение запросов на чтение\запись с большим кол-вом колонок. Пример: ```scala for (rv <- new ASelect { val nParentLevel = asInt("nParentLevel") val gidParent = asString("gidParent") val gidChild = asString("gidChild") val idParent = asLong("idParent") SQL""" select nParentLevel,gidParent,gidChild,idParent,idChild from table """ }) { println(rv.nParentLevel()) //запрос поля без его предварительного объявления println(rv.get("idChild").asNLong()) } ``` ## Практика ## Создание классов для модуля библиотека Задача: Создать классы: - Справочник: `Lbr_Publisher` - Справочник: `Lbr_Author` - Справочник: `Lbr_Book` ### Справочник `Lbr_Publisher` - Издатель | Системное имя | Наименование | Тип данных атрибута | Тип атрибута | Примечание | |---------------|---------------|---------------------|--------------|------------| | gid | gid | Varchar | Basic | | | sSystemName | Системное имя | Varchar | Basic | Мнемокод | | sCaption | Наименование | Varchar | Basic | Заголовок | | sDescription | Описание | Varchar | Basic | ```{tip} Создайте класс по адресу `lbr/src/main/resources/ru/bitec/app/lbr/Lbr_Publisher.odm.xml` где lbr наименование модуля ``` ### Справочник `Lbr_Author` - Автор В классе включить группировку | Системное имя | Наименование | Тип данных атрибута | Тип атрибута | Обязательный | Примечание | |---------------|--------------|---------------------|--------------|--------------|----------------------------------------------------------| | gid | gid | Varchar | Basic | | | | sCode | Код | Varchar | Basic | V | Мнемокод | | sLastName | Фамилия | Varchar | Basic | V | | | sFirstName | Имя | Varchar | Basic | V | | | sMiddleName | Отчество | Varchar | Basic | V | | | sFIO | ФИО | Varchar | Basic | | Заголовок | ```{tip} Создайте класс по адресу `lbr/src/main/resources/ru/bitec/app/lbr/Lbr_Author.odm.xml` где lbr наименование модуля ``` ### Справочник `Lbr_Book` - Книга | Системное имя | Наименование | Тип данных атрибута | Тип атрибута | Примечание | |---------------|----------------|---------------------|--------------|-------------------------------| | gid | gid | Varchar | Basic | | | sISBN | ISBN | Varchar | Basic | Мнемокод | | sCaption | Наименование | Varchar | Basic | Заголовок | | idPublisher | Издатель | Long | refObject | Ссылка на класс Lbr_Publisher | | idAuthor | Автор | Long | refObject | Ссылка на класс Lbr_Author | | nYear | Год издания | Number | basic | | | nColPage | Кол-во страниц | Number | basic | | | sDesc | Описание | Varchar | basic | ```{tip} Создайте класс по адресу `lbr/src/main/resources/ru/bitec/app/lbr/Lbr_Book.odm.xml` где lbr наименование модуля ``` ## Создание выборки основного меню приложения Создайте создайте основную выборку приложения `Библиотека` 1. Создайте файл `src/main/resources/ru/bitec/app/lbr/Lbr_MainMenu.avm.xml` ```xml ``` 2. Создайте файл `src/main/scala/ru/bitec/app/lbr/Lbr_MainMenuAvi.scala` ```scala package ru.bitec.app.lbr import ru.bitec.app.bs._ import ru.bitec.app.btk._ import ru.bitec.app.gtk.gl.avi.Visibilities import ru.bitec.gtk.core.AvmFile @AvmFile(name = "Lbr_MainMenu.avm.xml") object Lbr_MainMenuAvi extends Bs_ApplicationAvi { override def default(): Default = { new Default { override def meta = this } } trait Default extends super.Default { override def onLoadMeta(): Unit = { super.onLoadMeta() } //========== Справочники ====================== @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, imageCollection="Btk_Application", imageIndex=6, visibleOnMainMenu = Visibilities.Visible, caption = "Справочники", order = 10) def mm_ReferenceRoot(): Unit = {} //-------Подразделения----- @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Подразделения", order = 10, headOperation = "mm_ReferenceRoot") def mm_DepartmentRoot(): Unit = {} @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Подразделения", order = 5, headOperation = "mm_DepartmentRoot") def mm_Department(): Unit = { Bs_DepartmentAvi.tree().newForm().open() } @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Организации", order = 10, headOperation = "mm_DepartmentRoot") def mm_Bs_DepOwner(): Unit = { Bs_DepOwnerAvi.list().newForm().open() } @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Виды подразделений", order = 20, headOperation = "mm_DepartmentRoot") def mm_Bs_DepartmentSort(): Unit = { Bs_DepartmentSortAvi.list().newForm().open() } @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "-", order = 20, headOperation = "mm_ReferenceRoot") def mm_Delimiter1(): Unit = {} // Контрагенты @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Контрагенты", order = 30, headOperation = "mm_ReferenceRoot") def mm_Bs_ContrasRoot(): Unit = {} @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Контрагенты", order = 10, headOperation = "mm_Bs_ContrasRoot") def mm_Bs_Contras(): Unit = { Bs_ContrasAvi.mainList().newForm().open() } @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Типы контрагентов", order = 20, headOperation = "mm_Bs_ContrasRoot") def mm_Bs_ContrasType(): Unit = { Btk_ObjectTypeAvi.list().newForm().params(Map( "FLT_SREFCLASSNAME" -> "Bs_Contras", "FLT_SREFCLASSNAME.readOnly" -> true )).open() } @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "-", order = 30, headOperation = "mm_ReferenceRoot") def mm_Delimiter11(): Unit = {} @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Физические лица", order = 50, headOperation = "mm_ReferenceRoot") def mm_Bs_Person(): Unit = { Bs_PersonAvi.list().newForm().open() } //---------------------------------- @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "-", order = 60, headOperation = "mm_ReferenceRoot") def mm_Delimiter12(): Unit = {} @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Авторы", order = 80, headOperation = "mm_ReferenceRoot") def mm_Lbr_Autor(): Unit = { Lbr_AuthorAvi.list().newForm().open() } @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Издательства", order = 90, headOperation = "mm_ReferenceRoot") def mm_Lbr_Publisher(): Unit = { Lbr_PublisherAvi.list().newForm().open() } @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Книги", order = 110, headOperation = "mm_ReferenceRoot") def mm_Lbr_Book(): Unit = { Lbr_BookAvi.list().newForm().open() } //========== Документы ====================== @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, imageCollection="Btk_Application", imageIndex=7, visibleOnMainMenu = Visibilities.Visible, caption = "Документы", order = 20) def mm_DocumentRoot(): Unit = {} //========== Отчеты ====================== @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, imageCollection="Btk_Application", imageIndex=8, visibleOnMainMenu = Visibilities.Visible, caption = "Отчеты", order = 50) def mm_LbrReportRoot(): Unit = {} //========== Настройки ====================== @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, imageCollection="Btk_Application", imageIndex=9, visibleOnMainMenu = Visibilities.Visible, caption = "Настройки", order = 60) def mm_SettingRoot(): Unit = {} //========== Настройки документации ====================== @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Настройки документации", order = 10, headOperation = "mm_SettingRoot") def mm_DocSetting(): Unit = {} @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Типы объектов", order = 10, headOperation = "mm_DocSetting") def mm_Btk_ObjectType(): Unit = { Btk_ObjectTypeAvi.list().newForm().open() } @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Настройка детализации типов объектов", order = 15, headOperation = "mm_DocSetting") def mm_Btk_ObjectTypeDet(): Unit = { Btk_ObjectTypeDetAvi.list().newForm().open() } @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Жизненные циклы", order = 40, headOperation = "mm_DocSetting") def mm_Btk_LifeCycle(): Unit = { Btk_LifeCycleAvi.list().newForm().open() } @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Закладки", order = 60, headOperation = "mm_DocSetting") def mm_Btk_Tab(): Unit = { Btk_TabAvi.list().newForm().open() } //========== Печатные формы ============================== @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Печатные формы", order = 90, headOperation = "mm_SettingRoot") def mm_Rpt_Report(): Unit = { val vRpt_ReportAvi = Btk_Lib().getAviBySimpleName("Rpt_ReportAvi") if (vRpt_ReportAvi != null) vRpt_ReportAvi.list().newForm().open() } @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "-", order = 120, headOperation = "mm_SettingRoot") def mm_Delimiter60(): Unit = {} //============ Дополнительно ========================= @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Дополнительно", order = 130, headOperation = "mm_SettingRoot") def mm_AdditionalSettings(): Unit = {} @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Групировка объектов", order = 130, headOperation = "mm_AdditionalSettings") def mm_Btk_Group(): Unit = { Btk_GroupAvi.list().newForm().open() } @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Состояния классов", order = 150, headOperation = "mm_AdditionalSettings") def mm_Btk_ClassList(): Unit = { Btk_ClassListAvi.list().newForm().open() } //========== Менеджер заданий ====================== @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "-", order = 140, headOperation = "mm_SettingRoot") def mm_Delimiter140(): Unit = {} @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Менеджер заданий", order = 150, headOperation = "mm_SettingRoot") def mm_Btk_Job(): Unit = { Btk_JobGroupAvi.tree().newForm().open() //Btk_JobAvi.tree().newForm().open() } //========== Тестирование и отладка ====================== @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "-", order = 9998, headOperation = "mm_SettingRoot") def mm_Delimiter9998(): Unit = {} @Oper(visibleOnContextMenu = Visibilities.Invisible, visibleOnToolbar = Visibilities.Invisible, visibleOnMainMenu = Visibilities.Visible, caption = "Тесты и отладка", order = 9999, headOperation = "mm_SettingRoot") def mm_Testing(): Unit = {} } //--------- } ``` 3. Создайте файл `src/main/resources/META-INF/applications.xml` ```xml ```