Объектное хранилище

Платформа БМ #

Платформа БМ предоставляет следующие возможности, отсутствующие в EMF:

  • транзакционное редактирование,
  • блокировки,
  • быстрый поиск объекта по идентификатору,
  • индексирование,
  • контроль за расходом памяти,
  • эффективное использование файловой системы,
  • парциальная загрузка данных.

Краткий пример использования #

Предположим, вы разрабатываете собственную систему управления задачами. Центральной сущностью такой системы будет задача: у задачи есть заголовок, описание, статус, ссылка на автора, ссылка на исполнителя, ссылки на блокирующие и блокируемые задачи и список комментариев. Комментарий к задаче также представляет собой отдельную сущность: у комментария есть текст, ссылка на автора и список комментариев-ответов. Кроме того, есть пользователи, которые выступают в роли авторов и исполнителей задач. Данные сущности и их взаимоотношения показаны на UML-диаграмме ниже:

UML-диаграмма

Для наглядности ниже показана логическая структура фрагмента модели данных системы управления задачами в формате JSON:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
{
    "users":
    [
        {
            "firstName": "Иван",
            "lastName": "Иванов",
            "login": "iivanov",
            "passwordHash": "3d4f2bf07dc1be38b20cd6e46949a1071f9d0e3d",
            "active": true
        },
        {
            "firstName": "Евгений",
            "lastName": "Петров",
            "login": "epetrov",
            "passwordHash": "b1b3773a05c0ed0176787a4f1574ff0075f7521e",
            "active": true
        },
        {
            "firstName": "John",
            "lastName": "Smith",
            "login": "jsmith",
            "passwordHash": "7c4a8d09ca3762af61e59520943dc26494f8941b",
            "active": true
        }
    ],
    "tasks":
    [
        {
            "title": "Интернационализация веб-сайта компании",
            "description": "Обеспечить возможность перевода содержимого на другие языки.",
            "status": "CLOSED",
            "creator": "iivanov",
            "assignee": "epetrov",
            "blockers": [],
            "blocked": ["Перевод веб-сайта на английский", "Перевод веб-сайта на испанский"],
            "comments":
            [
                {
                    "text": "Включена в список задач к релизу 1.2",
                    "creator": "iinanov"
                },
                {
                    "text": "@iivanov, просьба провести ревью кода, см. ветку i18n.",
                    "creator": "epetrov",
                    "replies":
                    {
                        "text": "Ревью завершил, критических замечаний нет.",
                        "creator": "iinanov"       
                    }
                }
            ]
        },
        {
            "title": "Перевод веб-сайта на английский",
            "description": "Перевод веб-сайта на английский язык.",
            "status": "IN_PROGRESS",
            "creator": "iivanov",
            "assignee": "jsmith",
            "blockers": ["Интернационализация веб-сайта компании"],
            "blocked": [],
            "comments": []
        },
        {
            "title": "Перевод веб-сайта на испанский",
            "description": "Перевод веб-сайта на испанский язык.",
            "status": "OPEN",
            "creator": "iivanov",
            "assignee": null,
            "blockers": ["Интернационализация веб-сайта компании"],
            "blocked": [],
            "comments": []
        }
    ]
}

Обратите внимание на следующие моменты:

  • Задача и ее комментарии образуют дерево. При этом задача является корневым объектом дерева, а комментарий — вложенным объектом задачи или комментария, ответом на который он является. Таким образом, в терминах объектно-ориентированного программирования объекты в родительских и дочерних узлах находятся в отношении композиции. Это отношение на диаграмме классов UML обозначается стрелкой с черным ромбом.
  • Хотя модель древовидная, в ней присутствуют и горизонтальные связи. Так, задача ссылается на блокирующие и блокируемые задачи, на создавшего задачу пользователя, а также на ответственного за ее выполнение. В данном случае они закодированы с помощью некоторых символических имен. Такие ссылки в EMF называются кросс-ссылками, а их семантика определяется разработчиком. На диаграммах классов они обозначаются стрелкой с белым ромбом.

Теперь, когда у вас есть концептуальное описание модели, вам необходимо описать ее в терминах понятных EMF. Прежде всего не забудьте включить в список зависимостей своего проекта пакеты из плагина org.eclipse.emf.ecore, а также пакет com._1c.g5.v8.bm.core из одноименного плагина. Для декларативного описания EMF-классов используйте язык Xcore. Создайте файл task-tracker.xcore и скопируйте в него код, представленный ниже.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@GenModel(
    loadInitialization="false",
    literalsInterface="true",
    nonNLSMarkers="true",
    prefix="TaskTrackerModel",
    copyrightText="Copyright (C) 2023, 1C",
    updateClasspath="false",
    featureDelegation="Reflective",
    rootExtendsClass="com._1c.g5.v8.bm.core.BmObject",
    rootExtendsInterface="com._1c.g5.v8.bm.core.IBmObject")
@Ecore(nsPrefix="tasktracker", nsURI="http://g5.1c.ru/v8/bm/samples/tasktracker")
package com.e1c.g5.v8.bm.samples.tasktracker.model
 
class Task
{
    String title
    String description
    TaskStatus status
    refers User creator
    refers User assignee
    refers Task[] blockers
    refers Task[] blocked
    contains Comment[] comments
}
 
class Comment
{
    String text
    refers User creator
    contains Comment[] replies
}
 
enum TaskStatus
{
    OPEN = 0,
    IN_PROGRESS = 1,
    CLOSED = 2
}
 
class User
{
    String firstName
    String lastName
    String login
    String password
    boolean active
}

Здесь вы видите стандартный подход к описанию EMF-модели с использованием Xcore. Единственный момент, на котором стоит заострить внимание, — это настройки генератора:

  • БМ поддерживает только рефлективное делегирование (стр. 8).
  • Классы модели должны расширять класс BmObject (стр. 9) и реализовывать интерфейс IBmObject (стр. 10).

Когда вы сохраните описание модели в файле xcore, EMF автоматически сгенерирует модель. При этом на выходе вы получите:

  • несколько классов и интерфейсов, реализующих собственно модель,
  • фабрику объектов модели,
  • а также классы, предоставляющие различную метаинформацию.

После того как модель описана, и по ней сгенерированы Java-классы, можете приступать к конфигурированию и старту платформы БМ:

1
2
3
4
5
BmPlatformConfiguration configuration = new BmPlatformConfiguration();
configuration.setPath(Files.createTempDirectory("task-tracker"));
configuration.setEPackages(List.of(EcorePackage.eINSTANCE, TaskTrackerModelPackage.eINSTANCE));
 
BmPlatform bmPlatform = BmPlatform.createPlatform(configuration);

Затем необходимо создать пространство имен и хранилище данных для пространства имен, после чего связать их. Пространства имен и хранилища данных используются в БМ для разделения объектов. Например, в случае с системой управления задачами вы можете создать пространство имен и хранилище для каждого проекта.

1
2
3
IBmNamespace ns = platform.createNamespace("corporatewebsite", new BmNamespaceConfiguration());
IBmNamespaceDataStore store = bmPlatform.createNamespaceDataStore("corporatewebsite-data");
platform.assignStore(ns, store);

Забегая вперед, скажем, что при разработке плагинов для EDT создавать платформу БМ, пространства имен и хранилища данных вам не придется: платформа EDT создаст их сама. Вам достаточно получить их, используя сервис IBmModelManager из пакета com._1c.g5.v8.dt.core.platform. Самостоятельное их создание может понадобиться при разработке тестов, не использующих общую тестовую инфраструктуру.

Теперь, когда все готово к работе, рассмотрите, как осуществляется процесс редактирования персистентной модели. Во-первых, для осуществления редактирования модели необходимо создать транзакцию. Во-вторых, чтобы сделать объект персистентным, его необходимо подключить к БМ.

Подключить объект к БМ можно двумя способами:

  • если объект является объектом верхнего уровня (top object в терминах БМ или root object в терминах EMF), то его подключение осуществляется посредством вызова метода IBmPlatformTransaction.attachTopObject;
  • если объект является вложенным (contained), то его нужно добавить в объект-контейнер, подключенный к модели.

Рассмотрите процесс создания пользователя:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public long createUser(IBmNamespace ns, String firstName, String lastName, String login, String passwordHash)
{
    IBmPlatformTransaction transaction = bmPlatform.beginReadWriteTransaction();
 
    long userId;
 
    try
    {
        User user = TaskTrackerModelFactory.eINSTANCE.createUser();
        transaction.attachTopObject(ns, user, "User." + login);
 
        user.setFirstName(firstName);
        user.setLastName(lastName);
        user.setLogin(login);
        user.setPasswordHash(passwordHash);
        user.setActive(true);
 
        userId = user.bmGetId();
    }
    catch (Throwable t)
    {
        transaction.rollback();
        throw t;
    }
 
    transaction.commit();
 
    return userId;
}

В этом отрывке кода вы видите следующие действия:

  • В стр. 3 вы начинаете транзакцию на чтение и запись посредством вызова метода beginReadWriteTransaction.

  • В стр. 9 вы создаете экземпляр класса User.

  • В стр. 10 вы подключаете экземпляр класса User к БМ как объект верхнего уровня, располагая его в созданном выше пространстве имен. Третьим параметром в метод attachTopObject передается FQN подключаемого объекта. FQN уникальным образом адресует объект верхнего уровня в пространстве имен. Таким образом, вы можете добавить объекты с одинаковым FQN в разных пространствах имен, но при попытке добавить объекты с одинаковым FQN в одном пространстве имен БМ выбросит исключение.

  • В стр. 12-16 вы наполняете объект User данными.

  • В стр. 18 вы получаете идентификатор объекта User, который назначила ему платформа БМ при подключении.

  • В стр. 26 вы фиксируете транзакцию, сохраняя, тем самым, произведенные изменения в долговременном хранилище.

  • В стр. 28 вы возвращаете идентификатор объекта, который затем может быть использован клиентом метода для получения созданного объекта.

  • В стр. 22 вы откатываете транзакцию в случае возникновения ошибки. Например, вызов метода attachTopObject в стр. 9 может выбросить BmFqnAlreadyInUseException в случае, если объект с указанным FQN уже существует.

Далее вы можете в любое время получить добавленные объекты, отредактировать их, добавить новые, связать их с уже существующими и так далее.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public long createTask(IBmNamespace ns, String title, String description, long creatorId)
{
    IBmPlatformTransaction transaction = bmPlatform.beginReadWriteTransaction();
 
    long taskId;
 
    try
    {
        User creator = (User)transaction.getObjectById(ns, creatorId);
        assert creator != null : "Invalid creator ID";
 
        Task task = TaskTrackerModelFactory.eINSTANCE.createTask();
        transaction.attachTopObject(ns, task, "Task." + removeNonAlphanumericChars(title));
 
        task.setTitle(title);
        task.setDescription(description);
        task.setStatus(TaskStatus.OPEN);
 
        task.setCreator(creator);
 
        taskId = task.bmGetId();
    }
    catch (Throwable t)
    {
        transaction.rollback();
        throw t;
    }
 
    transaction.commit();
 
    return taskId;
}

В этом отрывке кода вы видите следующие действия:

  • производится получение ранее созданного объекта User (стр. 9);
  • поскольку предполагается, что в системе управления задачами пользователи не удаляются, то, если пользователь не найден, производится выброс ошибки (стр. 10);
  • создается объект Task и подключается к БМ (стр. 12, 13);
  • объект Task наполняется данными (стр. 15, 17);
  • объекту Task в поле creator устанавливается ссылка на существующий экземпляр класса User (стр. 19).

Обратите внимание, что в стр. 13 FQN для объекта Task генерируется на основе строки, полученной путем удаления из заголовка всех символов, не являющихся буквами и цифрами. Стоит заметить, что это не очень хорошее решение, т. к.: во-первых, оно не гарантирует уникальность, во-вторых, пользы от такого FQN нет никакой, ведь его по очевидным причинам вряд ли получится использовать для поиска задач по заголовку (для этого лучше использовать отдельный индекс полнотекстового поиска). Соответственно, в реальной системе логичным решением было бы вообще не назначать FQN объекту Task.

Платформа #

Инициация любого взаимодействия клиентского кода с платформой БМ всегда осуществляется посредством вызова одного из методов класса BmPlatform. Таким образом, класс BmPlatform можно представить как точку входа, через которую вы получаете доступ к данным, находящимся под управлением платформы.

Создание платформы #

Создать экземпляр BmPlatform можно только посредством вызова статического метода BmPlatform.createPlatform(BmPlatformConfiguration). Данный метод создает платформу с настройками, переданными в объекте BmPlatformConfiguration. Следующие настройки являются обязательными и должны быть явно указаны пользователем:

  • path — путь к каталогу, в котором будут располагаться файлы платформы,
  • ePackages — коллекция EMF-пакетов, с объектами которых может работать БМ.

Остальные настройки являются опциональными. Таким образом, если вы не укажете какой-либо параметр, то БМ будет использовать стандартное значение. Вы можете указать следующие настройки:

  • lockWaitTimeout — максимальное время ожидания установки блокировки объекта в транзакции в миллисекундах.
  • checkpointPeriod — максимальное время между выполнением контрольных точек в миллисекундах.
  • walSizeThreshold — размер журнала предзаписи (англ. WAL, write-ahead log) в байтах, при превышении которого выполняется контрольная точка и журнал сбрасывается.
  • forceSync — включает режим принудительной синхронизации, в котором все записи в файлы хранилищ платформы сразу отражаются на диске.
  • enableMonitoring — включает режим мониторинга.
  • indexedAttributes — индексируемые атрибуты.
  • containedObjectIndexingRules — правила индексации вложенных объектов.
  • executorService — экземпляр java.util.concurrent.ExecutorService для выполнения служебных фоновых процессов.
  • failureListener — подписчик, который уведомляется при обнаружении сбоя в хранилище платформы.
  • logger — логгер.
  • unfinishedCommitProcessor — подписчик, который уведомляется о фиксациях транзакций, выполнение которых было прервано аварийным завершением предыдущей сессии работы БМ, и которые были завершены в рамках текущей сессии.
  • uriBuildContributors — компоненты, посредством поставки которых клиенты БМ могут настраивать процесс построения URI объектов БМ.
  • crossReferenceFilters — компоненты, посредством поставки которых клиенты БМ могут настраивать процесс фильтрации обратных ссылок.
  • externalUriResolvers — компоненты, посредством которых клиенты БМ могут обеспечивать возможность разрешения любых EMF-объектов, не хранящихся в БМ.
  • referencePersistenceContributors — компоненты для эффективного хранения и разрешения ссылок на любые EMF-объекты, не хранящиеся в БМ. Данный механизм является улучшенной альтернативой механизму externalUriResolvers.
  • attributeSerializers — сериализаторы атрибутов. Значение использованных терминов будет приведено далее в этом документе.

Обзор основных методов #

  • void close() — останавливает платформу. В самом начале этого метода устанавливается признак неактивности платформы: любые обращения к ней как в этом потоке, так и в других не допускаются.

  • IBmNamespaceDataStore getNamespaceDataStore(String name) — получает хранилище данных по имени.

  • IBmNamespaceDataStore createNamespaceDataStore(String name) — создает хранилище данных с указанным именем.

  • void deleteNamespaceDataStore(IBmNamespaceDataStore store) — удаляет хранилище данных с указанным именем.

  • IBmNamespace getNamespace(String name) — получает пространство имен по имени.

  • IBmNamespace createNamespace(String name, BmNamespaceConfiguration configuration) — создает пространство имен с указанными именем и настройками.

  • void assignStore(IBmNamespace namespace, IBmNamespaceDataStore store) — назначает хранилище пространству имен.

  • void unassignStore(IBmNamespace namespace) — отменяет назначение хранилища пространству имен.

  • void deleteNamespace(IBmNamespace namespace) — удаляет пространство имен.

  • IBmPlatformTransaction beginReadWriteTransaction() — начинает транзакцию на редактирование.

  • IBmPlatformTransaction beginReadOnlyTransaction() — начинает транзакцию на чтение.

  • IBmPlatformTransaction getCurrentTransaction() — получает транзакцию, открытую в текущем потоке.

Пространства имен и хранилища данных #

Как уже упоминалось в примере использования БМ в начале этого документа, при добавлении объектов в БМ необходимо указывать пространство имен, в котором будет размещен объект, а при получении — в котором он находится. Таким образом, пространства имен позволяют осуществлять разделение более или менее независимых групп объектов. Так, например, в 1С:EDT для каждого проекта создается собственное пространство имен. Такой подход позволяет: во-первых, решить проблему наличия объектов с одинаковыми именами (и, следовательно, именами FQN) в разных проектах, во-вторых, при выполнении операции удаления проекта удалять все данные разом вместо удаления объектов по одному (как в случае, если бы данные всех проектов хранились бы в одном пространстве имен). При этом пространства имен не являются полностью изолированными, так как объекты из одного пространства имен могут ссылаться на объекты из другого пространства имен. В 1С:EDT это используется, например, для обеспечения возможности ссылаться на объекты проекта конфигурации из проектов внешних отчетов, внешних обработок и расширений.

Также в примере было упомянуто, что, прежде чем начать работу с созданным пространством имен, его необходимо связать с хранилищем данных. Здесь у вас может возникнуть вопрос: зачем вообще разделять эти сущности? Почему недостаточно только пространств имен? Такое разделение было осуществлено для обеспечения возможности мгновенного выполнения подмены содержимого пространств имен. Например, это можно использовать для реализации быстрого переключения веток. Так, данные каждой ветки можно размещать в разных хранилищах, а при переключении веток отвязывать пространство имен от хранилища текущей ветки и привязывать к хранилищу целевой ветки. Таким образом, пространства имен можно рассматривать как средство логического разделения данных, а хранилища — физического.

Идентификация объектов #

Целочисленные идентификаторы #

Каждый объект БМ имеет свой собственный уникальный идентификатор типа long, который ему назначается в процессе добавления объекта в модель. Этот идентификатор не меняется на протяжении всего времени жизни объекта. Идентификаторы генерируются в соответствии с внутренней логикой БМ и на процесс их формирования вы не можете оказать влияния. Для получения объектов по целочисленному идентификатору интерфейс транзакции IBmPlatformTransaction предоставляет метод getObjectById(IBmNamespace, long).

FQN #

Кроме этого, у всех объектов верхнего уровня есть еще один идентификатор — FQN. «FQN» — не самое удачное название, но оно привычно для разработчиков 1C:EDT. FQN представляет собой обычную строку, которую вы задаете при добавлении объекта в модель посредством вызова метода attachTopObject(IBmNamespace, IBmObject, String) интерфейса IBmPlatformTransaction. В отличие от внутренних идентификаторов, FQN можно менять посредством вызова метода updateTopObjectFqn(IBmObject, String). Для получения объекта верхнего уровня по FQN интерфейс IBmPlatformTransaction также предоставляет метод getTopObjectByFqn(IBmNamespace, String).

URI #

Помимо целочисленного идентификатора и FQN, у объектов БМ есть еще URI. URI не являются полноценными идентификаторами, и для этого есть две причины:

  • Во-первых, URI не являются полностью самостоятельными, т. к. генерируются на базе FQN соответствующего объекта верхнего уровня.
  • Во-вторых, URI не предназначены для активного использования вами с целью получения объектов, хотя такая возможность есть (см. метод getObjectByUri(URI) на интерфейсе IBmPlatformTransaction). В первую очередь URI необходимы для кодирования кросс-ссылок при сохранении объектов в основном хранилище. Кросс-ссылки это все ссылки, не помеченные как containment.

URI объектов БМ строятся по общим правилам построения URI в EMF, т. е. URI объекта формируется путем добавления фрагмента к URI ресурса, в котором этот объект расположен.

С ресурсами в БМ связана одна особенность: БМ не оперирует понятием ресурсов, а оперирует только объектами. В связи с этим в БМ ресурсы являются некоторой синтетической сущностью, которая введена лишь для обеспечения минимально необходимой совместимости с EMF. Ресурс БМ связан один-к-одному с соответствующим объектом верхнего уровня. В терминах EMF можно сказать, что при добавлении в модель объекта верхнего уровня БМ автоматически создает для этого объекта ресурс и размещает объект в этом ресурсе. При этом вы не можете каким-либо образом повлиять на то, в каком ресурсе будет размещен объект БМ.

URI ресурса БМ строится по следующему шаблону:

bm://<пространство имен>/<FQN соответствующего объекта верхнего уровня>

Фрагменты URI формируются по следующим правилам:

  1. Фрагмент URI объекта верхнего уровня всегда содержит один символ — «/».
  2. Фрагмент URI вложенного объекта формируется из фрагмента контейнера и сегмента, однозначно идентифицирующего вложенный объект в контейнере, разделенных символом «/». Если контейнер является объектом верхнего уровня, то разделитель не добавляется.
  3. Сегмент фрагмента вложенного объекта содержит:
    • Имя feature контейнера, в которой содержится данный объект, если feature неколлекционная.
    • Имя feature и идентификатор объекта внутри коллекции, разделенные двоеточием, в случае если feature коллекционная.
  4. Идентификатором объекта внутри коллекции является строковое представление значения идентифицирующей feature объекта.
  5. Поиск идентифицирующей feature объекта производится по следующему алгоритму:

Проверяется, помечена ли коллекция аннотацией http://www.1c.ru/v8/bm/CollectionItemId:

  • Если да, то идентифицирующей feature является feature объекта, имя которой указано в аннотации в атрибуте feature.
  • Если нет, то проверяется, являются ли элементы коллекции экземплярами класса java.util.Map.Entry:
    • Если да, то идентифицирующей feature является feature с именем key.
    • Если нет, то проверяется, содержит ли объект feature с именем name:
      • Если да, то она считается идентифицирующей.
      • Если нет, то считается, что у объекта нет идентифицирующей feature, и URI для этого объекта построена быть не может.

В случае возникновения необходимости построения URI объекта БМ в прикладном коде (например, для создания прокси-объекта) крайне нежелательно производить построение URI вручную. Для этих целей стоит пользоваться методами, предоставляемыми утилитарным классом BmUriUtil.

Таким образом, у объектов User в приведенном в начале документа примере будут следующие URI:

bm://corporatewebsite/User.iivanov#/
bm://corporatewebsite/User.epetrov#/
bm://corporatewebsite/User.jsmith#/

У объектов Task — следующие:

bm://corporatewebsite/Task.Интернационализациявебсайтакомпании#/
bm://corporatewebsite/Task.Переводвебсайтанаанглийский#/
bm://corporatewebsite/Task.Переводвебсайтанаиспанский#/

А у объектов Comment URI нет, поскольку в классе Comment нет feature, по которой можно было бы отличить объекты внутри коллекций comments и replies.

Для демонстрации работы с URI вложенных объектов, вы можете расширить модель:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@GenModel(
    loadInitialization="false",
    literalsInterface="true",
    nonNLSMarkers="true",
    prefix="TaskTrackerModel",
    copyrightText="Copyright (C) 2023, 1C",
    updateClasspath="false",
    featureDelegation="Reflective",
    rootExtendsClass="com._1c.g5.v8.bm.core.BmObject",
    rootExtendsInterface="com._1c.g5.v8.bm.core.IBmObject")
@Ecore(nsPrefix="tasktracker", nsURI="http://g5.1c.ru/v8/bm/samples/tasktracker")
package com.e1c.g5.v8.bm.samples.tasktracker.model
 
annotation "http://www.1c.ru/v8/bm/CollectionItemId" as CollectionItemId
 
class Task
{
    String title
    String description
    TaskStatus status
    refers User creator
    refers User assignee
    refers Task[] blockers
    refers Task[] blocked
 
    @CollectionItemId(feature="creationTimestamp")
    contains Comment[] comments
    contains Attachment[] attachment
}
 
class Comment
{
    long creationTimestamp
    String text
    refers User creator
 
    @CollectionItemId(feature="creationTimestamp")
    contains Comment[] replies
}
 
class Attachment
{
    String name
    String path
}
 
enum TaskStatus
{
    OPEN = 0,
    IN_PROGRESS = 1,
    CLOSED = 2
}
 
class User
{
    String firstName
    String lastName
    String login
    String passwordHash
    boolean active
 
    contains Address address
}
 
class Address
{
    String streetAddress
    String streetAddress2
    String city
    String state
    String zipCode
}

Теперь у объекта Comment есть поле creationTimestamp, которое может использоваться для различения элементов внутри коллекции (естественно, с оговоркой, что в данном тестовом приложении мы не будем создавать комментарии с интервалом меньшим, чем период срабатывания таймера операционной системы). Таким образом у объектов Comment могут быть следующие URI:

bm://corporatewebsite/Task.Интернационализациявебсайтакомпании#/comments:1675078229613
bm://corporatewebsite/Task.Интернационализациявебсайтакомпании#/comments:1675078229613/replies:1675078291614
bm://corporatewebsite/Task.Интернационализациявебсайтакомпании#/comments:1675078229613/replies:1675078310390
bm://corporatewebsite/Task.Интернационализациявебсайтакомпании#/comments:1675078229613/replies:1675078310390/replies:1675078333230

Также у объекта Task появилась еще одна коллекция вложенных объектов — attachments. Объекты Attachment могут использоваться для прикрепления к задаче каких-либо важных файлов, например скриншотов или логов. URI объектов Attachment могут выглядеть следующим образом:

bm://corporatewebsite/Task.Ошибкаприпоиске#/attachments:Скриншот
bm://corporatewebsite/Task.Ошибкаприпоиске#/attachments:Логи

Кроме того, у объекта User появилось поле address. Нужен ли адрес пользователя в системе управления задачами — вопрос спорный, но в данном случае у объекта Address могут быть URI вроде следующих:

bm://corporatewebsite/User.iivanov#/address
bm://corporatewebsite/User.epetrov#/address

Транзакции #

Механизм транзакций является основным инструментом обеспечения доступа к данным БМ. В обязанности данного механизма входит обеспечение целостности данных, координация конкурентного доступа, управление видимостью модификаций и т. п.

Редактирование данных #

В текущем решении редактирование данных допустимо только в транзакциях. Далее описаны основные сценарии редактирования данных.

Создание нового объекта #

Создание объекта в хранилище сводится к созданию экземпляра EMF-объекта и подключению ее к модели. Объекты верхнего уровня подключаются посредством вызова метода IBmPlatformTransaction.attachTopObject, а вложенные объекты подключаются посредством добавления в подключенный к модели объект-контейнер:

1
2
3
4
5
6
7
8
9
IBmPlatformTransaction transaction = platform.beginReadWriteTransaction();
 
Task task = TaskTrackerModelFactory.eINSTANCE.createTask();
transaction.attachTopObject(namespace, task, taskFqn);
 
Comment comment = TaskTrackerModelFactory.eINSTANCE.createComment();
task.getComments().add(comment);
 
transaction.commit();

При подключении объекта к модели (неважно, является ли он объектом верхнего уровня или вложенным объектом) происходит подключение и всех его вложенных объектов. Таким образом, предыдущий пример можно переписать так, как показано ниже. Но при этом стоит отметить, что первый способ предпочтителен с точки зрения производительности.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
IBmPlatformTransaction transaction = platform.beginReadWriteTransaction();
 
Task task = TaskTrackerModelFactory.eINSTANCE.createTask();
 
Comment comment = TaskTrackerModelFactory.eINSTANCE.createComment();
task.getComments().add(comment);
 
transaction.attachTopObject(namespace, task, taskFqn);
 
transaction.commit();

Редактирование существующего объекта #

Для редактирования существующего объекта, необходимо получить этот объект в транзакции и изменить интересующие features и БМ-свойства. Объект можно получить одним из следующих способов:

  • Объект верхнего уровня можно получить по его FQN с помощью метода IBmPlatformTransaction.getTopObjectByFqn.
  • Любой объект можно получить по его внутреннему целочисленному идентификатору с помощью метода IBmPlatformTransaction.getObjectById.
  • Объект можно получить путем считывания значения одной из ссылок, указывающих на него (как containment, так и не containment),
  • Путем получения соответствующего транзакционного экземпляра по экземпляру, не привязанному к какой-либо транзакции.

Следующий пример демонстрирует описанные выше приемы:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Редактирование объекта верхнего уровня, полученного по FQN
IBmPlatformTransaction transaction1 = platform.beginReadWriteTransaction();
 
Task task = (Task)transaction1.getTopObjectByFqn(namespace, taskFqn);
task.setAssignee((User)transaction1.getObjectById(newAssigneeId));
 
transaction1.commit();
 
// Редактирование объекта, полученного по целочисленному идентификатору
IBmPlatformTransaction transaction2 = platform.beginReadWriteTransaction();
 
Task task = (Task)transaction2.getObjectById(namespace, taskId);
task.setStatus(TaskStatus.IN_PROGRESS);
 
transaction2.commit();
 
// Редактирование объекта, полученного путем прохода по ссылкам
IBmPlatformTransaction transaction3 = platform.beginReadWriteTransaction();
 
Task task = (Task)transaction3.getTopObjectByFqn(namespace, taskFqn);
Comment comment = task.getComments().get(0);
comment.setText(correctedText);
 
transaction3.commit();
 
// Получение транзакционного экземпляра объекта по экземпляру, не привязанному к какой-либо транзакции
Comment globalInstance;
 
IBmPlatformTransaction transaction4 = platform.beginReadWriteTransaction();
 
globalInstance = (Comment)transaction4.getObjectById(namespace, commentId);
 
transaction4.commit();
 
IBmPlatformTransaction transaction5 = platform.beginReadWriteTransaction();
 
Comment transactionalCounterpart = transaction5.toTransactionObject(globalInstance);
transactionalCounterpart.setText(correctedText);
 
transaction5.commit();

Удаление существующего объекта #

Как и при создании объектов, способ удаления объекта зависит от того, является ли он объектом верхнего уровня или вложенным объектом. Объекты верхнего уровня удаляются с помощью метода IBmPlatformTransaction.detachTopObject, а вложенные объекты удаляются путем удаления из контейнера. При удалении объекта (неважно, является ли он объектом верхнего уровня или вложенным объектом) рекурсивно удаляются все его вложенные объекты.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Удаление объекта верхнего уровня
// Вместе с задачей будут удалены также все комментарии к ней.
IBmPlatformTransaction transaction1 = platform.beginReadWriteTransaction();
 
Task task = (Task)transaction1.getTopObjectByFqn(namespace, taskFqn);
transaction1.detachTopObject(task);
 
transaction1.commit();
 
// Удаление вложенного объекта
// Комментарий удаляется путем удаления из containment-коллекции "comments"
IBmTransaction transaction2 = platform.beginReadWriteTransaction();
 
Task task = (Task)transaction2.getTopObjectByFqn(namespace, taskFqn);
for (Iterator<Comment> it = catalog.getComments().iterator(); it.hasNext();)
{
    Comment comment = it.next();
    if (isSpam(comment.getText()))
    {
        it.remove();
    }
}
 
transaction2.commit();

Блокировки #

В обязанности механизма транзакций входит координация конкурентного доступа к данным. Для предотвращения неконтролируемого редактирования объектов параллельными транзакциями при получении объекта (А) в транзакции (любым из перечисленных ранее способов) автоматически устанавливается блокировка на все containment-дерево, которому принадлежит объект А. Другими словами, блокируется объект верхнего уровня, соответствующий объекту А, и все его прямые и непрямые дочерние объекты. Таким образом, ни одна из параллельно выполняющихся транзакций не может помешать работе с полученным в транзакции объектом.

Снятие блокировок осуществляется по завершении транзакции. Транзакция может быть завершена с помощью методов commit или rollback.

Взаимные блокировки транзакций #

При таком подходе к блокированию объектов с ростом нагрузки неизбежно будут возникать взаимные блокировки транзакций. Поэтому при получении объекта, захваченного другой транзакций, перед вызовом метода ожидания освобождения данного объекта производится проверка, не приведет ли ожидание данного объекта к взаимной блокировке. Если приведет, то выбрасывается исключение BmDeadlockDetectedException. Важно понимать, что выброс BmDeadlockDetectedException — нормальное явление, сигнализирующее не об ошибке, а о необходимости завершить текущую транзакцию, чтобы дать беспрепятственно выполниться другим транзакциям. Таким образом, стандартным способом обработки BmDeadlockDetectedException является откат транзакции, затем повтор операции после небольшой задержки, включающей случайную составляющую.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
while (true)
{
    IBmPlatformTransaction transaction = bmPlatform.beginReadWriteTransaction();
    try
    {
        doSomethingInTransaction(transaction);
    }
    catch (BmDeadlockDetectedException e)
    {
        transaction.rollback();
        Thread.sleep(100 + random.nextInt(100));
        continue;
    }
    catch (Throwable t)
    {
        transaction.rollback();
        throw t;
    }
 
    transaction.commit();
    break;
}

Обратите внимание на следующие моменты:

  • Если где-то внутри метода doSomethingInTransaction исключение BmDeadlockDetectedException будет перехвачено и не проброшено выше, то процедура отката и повтора не будет выполнена.
  • Время задержки включает случайную составляющую, чтобы в самом неудачном случае, когда все блокирующие друг друга транзакции выбрасывают BmDeadlockDetectedException, их повторное выполнение было разнесено во времени для уменьшения вероятности повтора взаимной блокировки.
  • Исключение BmDeadlockDetectedException не содержит stack trace, т. к. оно не предназначено для записи в лог.

Следует заметить, что хотя выбросы BmDeadlockDetectedException в общем случае не сигнализируют об ошибках, они могут означать, что объектная модель спроектирована не очень удачно, либо, что не очень удачно организовано распараллеливание алгоритмов, осуществляющих работу с ней. Для уменьшения количества ситуаций взаимной блокировки транзакций следует придерживаться следующих правил:

  • Модель должна быть такой, чтобы работающим с ней алгоритмам не приходилось захватывать большое количество containment-деревьев.
  • В модели не должно быть таких объектов, доступ к которым требуется большинству алгоритмов.
  • Распараллеливание алгоритмов должно осуществляться таким образом, чтобы минимизировать пересечение множеств containment-деревьев, захватываемых параллельно выполняющимися ветвями алгоритма.

Таймауты #

Если при попытке получения объекта, захваченного другой транзакцией, время ожидания освобождения данного объекта превысило значение, указанное в настройках (см. настройку lockWaitTimeout в BmPlatformConfiguration), то выбрасывается исключение BmLockWaitTimeoutException. В отличие от BmDeadlockDetectedException, данное исключение сигнализирует об ошибке и причины его должны расследоваться. В частности, выброс данного исключения может говорить о следующих ошибках:

  • Зацикливание кода, захватившего объект.
  • Подвисшие транзакции (такая ситуация может возникнуть, если в коде нет ни блока catch, ни блока finally, которые осуществляют откат транзакции в случае возникновения ошибки).
  • Неадекватно малое значение lockWaitTimeout.

Поскольку данное исключение должно расследоваться, то его следует записывать в лог. Для обеспечения возможности расследования BmLockWaitTimeoutException включает подробное сообщение о проблеме и stack trace.

Транзакции на чтение #

Если предполагается, что в транзакции будет производиться только чтение данных, то имеет смысл создавать транзакцию на чтение. Для этого необходимо вызвать один из методов BmPlatform.beginReadOnlyTransaction. Использование транзакций на чтение обладает следующими преимуществами:

  • Во-первых, при попытке выполнения операции записи будет выброшено исключение, что позволяет на ранней стадии выявить ошибки программирования.
  • Во-вторых, работа в транзакции на чтение немного эффективнее, при этом и дальше будет вестись работа по оптимизации на данном направлении.

Легковесные транзакции на чтение #

Все виды транзакций БМ при первом получении объекта добавляют его в свой локальный кэш. При каждом запросе объекта сначала производится его поиск в локальном кэше транзакции (таким образом, локальный кэш транзакции является кэшем первого уровня), и, только если объект там не обнаружен, осуществляется поиск в хранилище. За счет этого повторные запросы объекта выполняются существенно быстрее. Однако, если в транзакции осуществляется обход больших графов объектов, локальный кэш транзакции может потреблять неприемлемо много памяти, что может привести к отказу приложения.

Чтобы решить эту проблему, в БМ были реализованы легковесные транзакции на чтение. Для того чтобы открыть легковесную транзакцию на чтение, необходимо вызвать метод beginReadOnlyTransaction(boolean) класса BmPlatform, передав в качестве параметра значение true. В легковесных транзакциях количество объектов, которые могут находиться в кэше одновременно, строго ограничено. При добавлении очередного объекта, в случае если кэш уже заполнен, производится вытеснение данных объекта (но не самого объекта), который не использовался дольше всех (англ. LRU, least recently used). При этом блокировка с вытесненного объекта не снимается. Таким образом, при повторном запросе объекта, данные которого были вытеснены, можно быть уверенным, что он все еще существует и состояние его не изменилось.

При этом все же не стоит злоупотреблять возможностью получать в транзакции большое количество объектов, поскольку, как было сказано выше, при вытеснении данных блокировки с объектов не снимаются. Таким образом, вероятность возникновения ситуации взаимной блокировки транзакций существенно повышается.

Выброс объектов из транзакций #

Для выброса объектов из транзакции интерфейс IBmPlatformTransaction предоставляет метод evict(IBmNamespace, long). При использовании этого метода необходимо учитывать следующие особенности:

  • Данный метод выбрасывает из транзакции все объекты containment-дерева (ресурса в терминах EMF), которому принадлежит указанный объект.
  • Если containment-дерево заблокировано на запись, то вызов игнорируется.
  • В отличие от автоматического вытеснения, реализованного в легковесных транзакциях, данный метод полностью выбрасывает объект из локального кэша транзакции, а также снимает с объекта блокировку.
  • Доступ к объектам, вытесненным из транзакции, запрещен.

В связи с тем, что при выполнении данного метода выбрасываются все объекты containment-дерева, данный метод довольно непредсказуем, поэтому не рекомендуется использовать его без крайней на то необходимости.

Транзакции и потоки #

БМ устанавливает следующие правила доступа к транзакциям:

  • Транзакции должны использоваться только в открывшем их потоке.
  • Объекты, полученные в транзакции, также должны использоваться только в том потоке, в котором они были получены.
  • В каждом потоке может быть одновременно открыто не более одной транзакции. Чтобы получить транзакцию, открытую в текущем потоке, класс BmPlatform предоставляет метод getCurrentTransaction.

Действия с побочными эффектами #

Рассмотрите следующий пример:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public void createTask(IBmNamespace ns, String title, String description, long creatorId)
{
    IBmPlatformTransaction transaction = bmPlatform.beginReadWriteTransaction();
  
    try
    {
        Task task = doCreateTask(transaction, ns, title, description, creatorId);
        messageQueue.put(new TaskCreatedMessage(task.bmGetId()));
    }
    catch (Throwable t)
    {
        transaction.rollback();
        throw t;
    }
  
    transaction.commit();
}

Что здесь может пойти не так? Сообщение о создании задачи может быть доставлено адресату до того, как завершена фиксация транзакции, либо фиксация может вовсе завершиться неудачей (например, из-за проблем с доступом к диску). Таким образом, адресат получит сообщение с идентификатором несуществующей задачи, что в дальнейшем может привести к ошибке обработки данного сообщения.

Чтобы исключить такую ситуацию, рассылку сообщений и любые другие действия с побочными эффектами необходимо осуществлять строго после фиксации транзакции, как показано ниже:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public void createTask(IBmNamespace ns, String title, String description, long creatorId)
{
    IBmPlatformTransaction transaction = bmPlatform.beginReadWriteTransaction();
  
    long taskId;
 
    try
    {
        Task task = doCreateTask(transaction, ns, title, description, creatorId);
        taskId = task.bmGetId();
    }
    catch (Throwable t)
    {
        transaction.rollback();
        throw t;
    }
  
    transaction.commit();
 
    messageQueue.put(new TaskCreatedMessage(taskId));
}

На случай, если код, в котором необходимо выполнить действие с побочным эффектом, не управляет открытием и закрытием транзакции, интерфейс IBmPlatformTransaction предоставляет метод addPostCommitHandler(Runnable). Ниже приведен пример его использования:

1
2
3
4
5
6
7
public void createTask(IBmPlatformTransaction transaction, IBmNamespace ns, String title, String description, long creatorId)
{
    Task task = doCreateTask(transaction, ns, title, description, creatorId);
    long taskId = task.bmGetId();
 
    transaction.addPostCommitHandler(() -> messageQueue.put(new TaskCreatedMessage(taskId)));
}

Доступ к данным вне транзакций #

Для обеспечения совместимости с механизмами, реализованными в ранних версиях 1C:EDT, в БМ была добавлена возможность доступа к данным вне транзакций. В частности, были добавлены следующие возможности:

  • Разрешена работа с объектами после фиксации (но не после отката) транзакции, в который они были получены.
  • Добавлен метод IBmEngine.getObjectById(long) для получения любого объекта по целочисленному идентификатору.
  • Добавлен метод IBmEngine.getTopObjectByFqn(String) для получения объекта верхнего уровня по FQN.

На самом деле, внутри методов IBmEngine.getObjectById(long) и IBmEngine.getTopObjectByFqn(String) создается “неявная” транзакция, в которой производится получение объекта. Она начинается и фиксируется “прозрачно” для вас. Информацию об интерфейсе IBmEngine см. в разделе Поддержка старого API.

С работой без явного создания транзакций связано несколько ограничений:

  • Редактирование объектов, полученных таким образом, запрещено.
  • При разрешении ссылок объекта, полученного таким образом, внутри БМ неявно создается транзакция. Соответственно, при обходе больших графов будет создаваться чрезмерное количество транзакций, что негативно скажется на производительности.
  • Поскольку транзакции нет, то объект не блокируется, соответственно, параллельно выполняющиеся потоки могут модифицировать его состояние, что может привести к ошибкам в текущем потоке. Рассмотрим следующий пример:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public List<String> getTaskComments(Task task)
{
    EList<Comment> comments = task.getComments();
    int commentCount = comments.size();
 
    List<String> result = new ArrayList<>(commentCount);
    for (int i = 0; i < commentCount; i++)
    {
        Comment comment = comments.get(i);
        result.add(comment.getText());
    }
 
    return result;
}

Очевидно, что если в параллель выполнению цикла другой поток удалит комментарий, то вызов comments.get(i) на последней итерации цикла приведет к выбросу IndexOutOfBoundsException. При этом, если переписать данный пример следующим образом, то такой проблемы не возникнет, т. к. задача будет заблокирована на чтение до завершения транзакции, и другие потоки не смогут ее модифицировать:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public List<String> getTaskComments(long taskId)
{
    IBmPlatformTransaction transaction = platform.beginReadOnlyTransaction();
    try
    {
        Task task = (Task)transaction.getObjectById(taskId);
        if (task == null)
        {
            return List.of();
        }
 
        EList<Comment> comments = task.getComments();
        int commentCount = comments.size();
        
        List<String> result = new ArrayList<>(commentCount);
        for (int i = 0; i < commentCount; i++)
        {
            Comment comment = comments.get(i);
            result.add(comment.getText());
        }
 
        return result;
    }
    finally
    {
        transaction.commit();
    }
}

В связи с описанными выше ограничениями крайне нежелательно работать с БМ без явного создания транзакций, даже если предполагается, что будет осуществляться доступ только на чтение.

Поиск объектов #

БМ поддерживает поиск объектов по различным критериям, например по целочисленному идентификатору или по FQN, уже упоминавшимся в данном документе. Помимо этого БМ поддерживает также поиск объектов по типу, по значению атрибутов; поиск объектов, ссылающихся на заданный; поиск объектов по FQN без учета регистра.

Поиск по типу #

Объекты верхнего уровня #

Для получения объектов верхнего уровня по типу интерфейс IBmPlatformTransaction предоставляет метод getTopObjectIterator(IBmNamespace, EClass). Ниже показан пример использования данного метода:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public List<String> getUserNames(IBmPlatformTransaction transaction, IBmNamespace namespace)
{
    List<String> result = new ArrayList<>();
    for (Iterator<IBmObject> it = transaction.getTopObjectIterator(namespace, TaskTrackerModelPackage.Literals.USER); it.hasNext();)
    {
        User user = (User)it.next();
        result.add(user.getFirstName() + " " + user.getLastName());
    }
 
    return result;
}

Вложенные объекты #

Аналогично, для получения вложенных объектов по типу интерфейс IBmPlatformTransaction предоставляет метод getContainedObjectIterator(IBmNamespace, EClass). При этом поиск поддерживает только те вложенные объекты, которые удовлетворяют заданным правилам индексации (см. настройку containedObjectIndexingRules в BmPlatformConfiguration). Необходимо заметить, что пользоваться данной функцией следует с большой осторожностью, потому что, как правило, количество вложенных объектов многократно превышает количество объектов верхнего уровня, соответственно, индекс вложенных объектов может оказаться слишком большим и требовать больших затрат на поддержку.

Поиск по значению атрибута #

Для поиска объектов по значению атрибута интерфейс IBmPlatformTransaction предоставляет метод getObjectsByAttributeValue(IBmNamespace, EAttribute, Object). При этом поиск поддерживает только индексируемые атрибуты (см. настройку indexedAttributes в BmPlatformConfiguration). Например, вы можете воспользоваться данной функцией для организации поиска задач по статусу. Для этого добавьте атрибут status в список индексируемых при создании платформы:

1
2
3
4
5
BmPlatformConfiguration configuration = new BmPlatformConfiguration();
// ...
configuration.setIndexedAttributes(List.of(TaskTrackerModelPackage.Literals.TASK__STATUS));
  
BmPlatform bmPlatform = BmPlatform.createPlatform(configuration);

Теперь все задачи будут индексироваться по значению атрибута status. Ниже приведен пример использования метода getObjectsByAttributeValue:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public List<String> getTasksByStatus(IBmPlatformTransaction transaction, IBmNamespace namespace, TaskStatus status)
{    
    List<String> result = new ArrayList<>();
    for (Iterator<IBmObject> it = transaction.getObjectsByAttributeValue(namespace, TaskTrackerModelPackage.Literals.TASK__STATUS, status); it.hasNext();)
    {
        Task task = (Task)it.next();
        result.add(task.getTitle());   
    }
 
    return result;
}

Необходимо заметить, что БМ позволяет производить поиск и по коллекционным (помеченным как many) атрибутам. В этом случае, объект может быть получен по любому из элементов коллекции. Пользуясь этим, можно, например, реализовать поиск задач по тегу. Сначала добавьте поле tags в класс Task:

1
String[] tags

Добавьте его в список индексируемых атрибутов:

1
2
3
4
5
6
BmPlatformConfiguration configuration = new BmPlatformConfiguration();
// ...
configuration.setIndexedAttributes(List.of(TaskTrackerModelPackage.Literals.TASK__STATUS,
                                           TaskTrackerModelPackage.Literals.TASK__TAGS));
  
BmPlatform bmPlatform = BmPlatform.createPlatform(configuration);

Поиск в этом случае осуществляется аналогично поиску по статусу:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public List<String> getTasksByTag(IBmPlatformTransaction transaction, IBmNamespace namespace, String tag)
{
    List<String> result = new ArrayList<>();
    for (Iterator<IBmObject> it = transaction.getObjectsByAttributeValue(namespace, TaskTrackerModelPackage.Literals.TASK__TAGS, tag); it.hasNext();)
    {
        Task task = (task)it.next();
        result.add(task.getTitle());
    }
 
    return result;
}

Поиск объектов, ссылающихся на заданный #

БМ предоставляет возможность поиска кросс-ссылок (т. е. ссылок не помеченных как containment), указывающих на заданный объект. Для этого БМ предоставляет следующие методы:

  • IBmPlatformTransaction.getReferences(IBmObject, IBmNamespace),
  • IBmPlatformTransaction.getReferences(URI, IBmNamespace),
  • IBmObject.bmGetReferences().

Первый метод возвращает ссылки на указанный объект из объектов, принадлежащих указанному пространству имен; второй метод — на объект с указанным URI. Третий метод возвращает ссылки из объектов, принадлежащих тому же пространству имен, что и объект, на котором производится вызов метода.

Стоит отметить, что данные методы могут возвращать не все имеющиеся ссылки на указанный объект: возвращаются только отслеживаемые ссылки. По умолчанию БМ отслеживает все ссылки кроме «прямых» (см. раздел Прямые ссылки). Кроме того, в БМ есть возможность фильтрации ссылок: в случае если пользователю БМ заранее известно, что для определенного класса объектов нет необходимости в отслеживании ссылок, пользователь может реализовать интерфейс IBmCrossReferenceFilter и добавить его в список фильтров (см. настройку crossReferenceFilters в BmPlatformConfiguration).

В случае если бы у объекта Task не было поля blocked, вы могли бы воспользоваться функцией поиска ссылок для поиска всех заблокированных задач.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public List<Task> getBlockedTasks(IBmPlatformTransaction transaction, IBmNamespace namespace, Task task) {
    List<Task> result = new ArrayList<>();
        
    Collection<IBmCrossReference> references = transaction.getReferences(task, namespace);
    for (IBmCrossReference reference : references)
    {
        if (reference.getFeature() == TaskTrackerModelPackage.Literals.TASK__BLOCKERS)
        {
            Task blockedTask = (Task)reference.getObject();
            if (blockedTask != null)
            {
                result.add(blockedTask);
            }
        }
    }
 
    return result;
}

Приведенная выше функция getBlockedTasks собирает все задачи, у которых переданная в качестве параметра задача находится в коллекции blockers, т. е. является блокирующей.

Однако, поскольку у объекта Task есть поле blocked, то смысла в отслеживании ссылок из коллекции blockers нет, поэтому имеет смысл отключить его из соображения экономии ресурсов. Для этого вы можете реализовать свой фильтр кросс-ссылок:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public final class BlockersReferenceFilter
    implements IBmCrossReferenceFilter
{
    @Override
    public boolean isCrossReferenceFiltered(EClass toObjectEClass, EClass fromObjectEClass)
    {
        return toObjectEClass == TaskTrackerModelPackage.Literals.TASK
            && fromObjectEClass == TaskTrackerModelPackage.Literals.TASK;
    }
}

Данный класс фильтрует все ссылки из объектов типа Task на объекты этого же типа. Таким образом, БМ теперь не будет отслеживать ссылки ни из коллекции blockers, ни из коллекции blocked.

Не забудьте указать реализованный фильтр в настройках при создании платформы БМ:

1
2
3
4
5
BmPlatformConfiguration configuration = new BmPlatformConfiguration();
// ...
configuration.setCrossReferenceFilter(List.of(new BlockersReferenceFilter()));
   
BmPlatform bmPlatform = BmPlatform.createPlatform(configuration);

Поиск по FQN без учета регистра #

Также БМ позволяет производить поиск объектов по FQN без учета регистра. Для этого интерфейс IBmPlatformTransaction предоставляет метод getTopObjectsByFqnIgnoreCase(IBmNamespace, String).

Прямые ссылки #

По умолчанию БМ, как и EMF, в качестве значений кросс-ссылок хранит URI целевого объекта, если целевой объект хранится в БМ, и у него есть URI (о том, как хранятся кросс-ссылки на объекты, не хранящиеся в БМ, см. раздел Ссылки на внешние объекты). Если объект хранится в БМ, но у него нет URI, то в качестве значения используется его целочисленный идентификатор.

Использование URI в качестве внутреннего представления кросс-ссылок позволяет производить установку ссылки на еще не существующий в БМ объект. В этом случае в метод установки значения кросс-ссылки передается прокси-объект, содержащий URI целевого объекта. Например, это удобно, если вы в системе управления задачами работаете над возможностью загрузки данных из какой-либо другой системы. В этом случае вам не требуется ни сортировать задачи на загрузку так, чтобы все объекты загружались строго после тех объектов, на которые они ссылаются, ни делать загрузку в два прохода, когда сначала производится загрузка основных данных, а потом производится связывание объектов.

Однако, использование URI в качестве значения кросс-ссылок имеет свои недостатки:

  • URI могут быть достаточно громоздкими, соответственно могут приводить к существенному потреблению оперативной памяти и дискового пространства.
  • Получение объекта по URI — достаточно сложный процесс. Сначала необходимо получить объект верхнего уровня по FQN, затем пройти по всей цепочке вложенных объектов до целевого, если целевой объект вложенный.

В связи с этим в БМ предусмотрена возможность принудительного использования целочисленного идентификатора в качестве значения ссылки, как в случае, когда у объекта нет URI. Для этого в БМ предусмотрена аннотация http://www.1c.ru/v8/bm/ForceDirectReference. Рассмотрите использование данной аннотации на примере:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
annotation "http://www.1c.ru/v8/bm/ForceDirectReference" as ForceDirectReference

class Task
{
    String title
    String description
    TaskStatus status
    refers User creator
    refers User assignee
    @ForceDirectReference
    refers Task[] blockers
    @ForceDirectReference
    refers Task[] blocked
    contains Comment[] comments
}

Теперь ссылки на блокирующие и блокируемые задачи в качестве значения будут хранить целочисленный идентификатор. Таким образом, хранилище будет компактнее и разрешение ссылок будет происходить быстрее.

В свою очередь использование «прямых» ссылок обладает своими недостатками:

  • Нельзя установить ссылку на еще не существующий объект.
  • Если ссылка установлена, а затем целевой объект удаляется, то при получении значения не будет возможности вернуть прокси.
  • Прямые ссылки не отслеживаются (см. раздел Поиск объектов, ссылающихся на заданный).

Обертки типов #

Работа с массивами #

Предположим, вам не нравится, что хэш-значение пароля хранится в виде строки, и вы хотите, чтобы оно хранилось как байтовый массив. Однако, если вы модифицируете модель, как показано ниже, то не достигнете желаемого результата:

1
2
3
4
5
6
7
8
class User
{
    String firstName
    String lastName
    String login
    byte[] passwordHash
    boolean active
}

Вместо геттера и сеттера для поля типа byte[] EMF создает геттер списка объектов типа java.lang.Byte. Собственно, это ожидаемо, т. к. это стандартный подход генерации кода для полей помеченных как many.

1
2
3
4
5
public interface User extends IBmObject
{
    // ...
    EList<Byte> getPasswordHash();
}

Однако many-поле вам не требуется, т. к. вам вряд ли понадобится манипулировать отдельными байтами хэш-значения, поэтому в данном случае лучше воспользоваться обертками типов. Измените модель следующим образом:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type ByteArray wraps byte[]
 
class User
{
    String firstName
    String lastName
    String login
    ByteArray passwordHash
    boolean active
}

Теперь EMF сгенерирует код, который вам требуется:

1
2
3
4
5
6
public interface User extends IBmObject
{
    // ...
    byte[] getPasswordHash();
    void setPasswordHash(byte[] value);
}

Для того чтобы БМ умела сохранять и загружать поля типа ByteArray, реализуйте соответствующий сериализатор:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public final class ByteArrayAttributeSerializer
    implements IBmAttributeSerializer
{
    @Override
    public EDataType getSupportedDataType()
    {
        return TaskTrackerModelPackage.Literals.BYTE_ARRAY;
    }
 
    @Override
    public void serialize(Object value, EAttribute attribute, DataOutput output) throws IOException
    {
        byte[] byteArray = (byte[])value;
        output.writeInt(byteArray.length);
        output.write(byteArray);
    }
 
    @Override
    public Object deserialize(EAttribute attribute, DataInput input) throws IOException
    {
        byte[] byteArray = new byte[input.readInt()];
        input.readFully(byteArray);
        return byteArray;
    }
}

Далее при создании платформы БМ укажите его в настройках:

1
2
3
4
5
BmPlatformConfiguration configuration = new BmPlatformConfiguration();
// ...
configuration.setAttributeSerializers(List.of(new ByteArrayAttributeSerializer()));
    
BmPlatform bmPlatform = BmPlatform.createPlatform(configuration);

Теперь вы можете переписать метод создания пользователя следующим образом:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public long createUser(IBmNamespace ns, String firstName, String lastName, String login, String password)
{
    IBmPlatformTransaction transaction = bmPlatform.beginReadWriteTransaction();
  
    long userId;
  
    try
    {
        User user = TaskTrackerModelFactory.eINSTANCE.createUser();
        transaction.attachTopObject(ns, user, "User." + login);
  
        user.setFirstName(firstName);
        user.setLastName(lastName);
        user.setLogin(login);
        user.setPasswordHash(computeHash(password));
        user.setActive(true);
  
        userId = user.bmGetId();
    }
    catch (Throwable t)
    {
        transaction.rollback();
        throw t;
    }
  
    transaction.commit();
  
    return userId;
}
 
private byte[] computeHash(String str)
{
    // ...
}

Использование неизменяемых объектов #

Важно понимать, что БМ не детектирует изменения во внутренней структуре значений атрибутов. Например, при выполнении следующего метода в хранилище БМ не будет внесено никаких изменений. Конечно, вряд ли бы кто-то захотел обновить пароль подобным образом, но, если бы в качестве эксперимента решил попробовать, то ничего бы не вышло:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void setUserPassword(IBmNamespace ns, long userId, String newPassword)
{
    IBmPlatformTransaction transaction = bmPlatform.beginReadWriteTransaction();
  
    try
    {
        User user = (User)transaction.getObjectById(userId);
        assert user != null : "Invalid user ID";
 
        byte[] currentPasswordHash = user.getPasswordHash();
        byte[] newPasswordHash = computeHash(newPassword);
 
        for (int i = 0; i < newPasswordHash.length; i++)
        {
           currentPasswordHash[i] = newPasswordHash[i];
        }
    }
    catch (Throwable t)
    {
        transaction.rollback();
        throw t;
    }
  
    transaction.commit();
}

Правильное решение приведено ниже:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public void setUserPassword(IBmNamespace ns, long userId, String newPassword)
{
    IBmPlatformTransaction transaction = bmPlatform.beginReadWriteTransaction();
  
    try
    {
        User user = (User)transaction.getObjectById(userId);
        assert user != null : "Invalid user ID";
 
        user.setPassword(computeHash(newPassword));
    }
    catch (Throwable t)
    {
        transaction.rollback();
        throw t;
    }
  
    transaction.commit();
}

Во избежание подобных ситуаций рекомендуется в качестве значений атрибутов использовать неизменяемые объекты (англ. immutable). Продемонстрируем на примере. Сначала реализуйте класс PasswordHash, как показано ниже:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final class PasswordHash
{
    private final byte[] bytes;
 
    public PasswordHash(byte[] bytes)
    {
        if (bytes == null)
        {
            throw new IllegalArgumentException("Argument 'bytes' may not be null");
        }
 
        this.bytes = Arrays.copyOf(bytes, bytes.length);
    }
 
    public boolean match(byte[] otherPasswordHashBytes)
    {
        return Arrays.equals(bytes, otherPasswordHashBytes);
    }
 
    public byte[] toBytes()
    {
        return Arrays.copyOf(bytes, bytes.length);
    }
}

Обновите модель:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type PasswordHash wraps PasswordHash
 
class User
{
    String firstName
    String lastName
    String login
    PasswordHash passwordHash
    boolean active
}

Реализуйте сериализатор:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public final class PasswordHashAttributeSerializer
    implements IBmAttributeSerializer
{
    @Override
    public EDataType getSupportedDataType()
    {
        return TaskTrackerModelPackage.Literals.PASSWORD_HASH;
    }
 
    @Override
    public void serialize(Object value, EAttribute attribute, DataOutput output) throws IOException
    {
        byte[] byteArray = ((PasswordHash)value).toBytes();
        output.writeInt(byteArray.length);
        output.write(byteArray);
    }
 
    @Override
    public Object deserialize(EAttribute attribute, DataInput input) throws IOException
    {
        byte[] byteArray = new byte[input.readInt()];
        input.readFully(byteArray);
        return new PasswordHash(byteArray);
    }
}

Обновите конфигурацию платформы БМ:

1
2
3
4
5
BmPlatformConfiguration configuration = new BmPlatformConfiguration();
// ...
configuration.setAttributeSerializers(List.of(new PasswordHashAttributeSerializer()));
    
BmPlatform bmPlatform = BmPlatform.createPlatform(configuration);

Внесите изменения в метод установки пароля:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public void setUserPassword(IBmNamespace ns, long userId, String newPassword)
{
    IBmPlatformTransaction transaction = bmPlatform.beginReadWriteTransaction();
  
    try
    {
        User user = (User)transaction.getObjectById(userId);
        assert user != null : "Invalid user ID";
 
        user.setPassword(new PasswordHash(computeHash(newPassword)));
    }
    catch (Throwable t)
    {
        transaction.rollback();
        throw t;
    }
  
    transaction.commit();
}

Обертки типов как альтернатива вложенным объектам #

В разделе, посвященном идентификации объектов, в классе User мы добавили containment-ссылку на объект Address для хранения адреса пользователя. Хотя такой подход и является правильным с точки зрения EMF, он не является самым эффективным при использовании БМ. Сначала объекту Address при подключении к БМ будет назначен идентификатор, далее объект будет сохранен в виде отдельной записи на диске, после чего в специальный индекс будет добавлена запись, сопоставляющая идентификатор объекта и его физическое расположение. Кроме того, при запросе поля address у объекта User сначала будет получен идентификатор, посредством которого закодирована ссылка на адрес, после чего по идентификатору будет получен объект, для чего сначала будет произведен поиск в индексе информации о физическом расположении записи, и только затем будет произведено чтение объекта из этой записи.

Поскольку в действительности вряд ли кому-то может понадобиться возможность прямого получения объекта Address по идентификатору, то кажется вполне логичным не тратить ресурсы на хранение отдельной записи и обновление индекса. Вместо этого лучше хранить объект Address вместе с объектом User, которому он принадлежит. Этого можно добиться с помощью типов оберток. Для начала реализуйте Address как обычный Java-класс:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public final class Address
{
    private final String streetAddress;
    private final String streetAddress2;
    private final String city;
    private final String state;
    private final String zipCode;
 
    Address(String streetAddress, String streetAddress2, String city, String state, String zipCode)
    {
        assert streetAddress != null : "Argument 'streetAddress' may not be null";
        assert streetAddress2 != null : "Argument 'streetAddress2' may not be null";
        assert city != null : "Argument 'city' may not be null";
        assert state != null : "Argument 'state' may not be null";
        assert zipCode != null : "Argument 'zipCode' may not be null";
 
        this.streetAddress = streetAddress;
        this.streetAddress2 = streetAddress2;
        this.city = city;
        this.state = state;
        this.zipCode = zipCode;
    }
 
    public String getStreetAddress()
    {
        return streetAddress;
    }
 
    public String getStreetAddress2()
    {
        return streetAddress2;
    }
 
    public String getCity()
    {
        return city;
    }
 
    public String getState()
    {
        return state;
    }
 
    public String getZipCode()
    {
        return zipCode;
    }
}

Добавьте обертку для класса Address и поле address в класс User:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type PasswordHash wraps PasswordHash
type Address wraps Address
 
class User
{
    String firstName
    String lastName
    String login
    PasswordHash passwordHash
    Address address
    boolean active
}

Реализуйте сериализатор:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public final class AddressAttributeSerializer
    implements IBmAttributeSerializer
{
    @Override
    public EDataType getSupportedDataType()
    {
        return TaskTrackerModelPackage.Literals.ADDRESS;
    }
 
    @Override
    public void serialize(Object value, EAttribute attribute, DataOutput output) throws IOException
    {
        Address address = (Address)value;
        output.writeUTF(address.getStreetAddress());
        output.writeUTF(address.getStreetAddress2());
        output.writeUTF(address.getCity());
        output.writeUTF(address.getState());
        output.writeUTF(address.getZipCode());
    }
 
    @Override
    public Object deserialize(EAttribute attribute, DataInput input) throws IOException
    {
        String streetAddress = input.readUTF();
        String streetAddress2 = input.readUTF();
        String city = input.readUTF();
        String state = input.readUTF();
        String zipCode = input.readUTF();
 
        return new Address(streetAddress, streetAddress2, city, state, zipCode);
    }
}

Далее, так же как и в случае с паролем, укажите сериализатор при создании платформы БМ:

1
2
3
4
5
6
BmPlatformConfiguration configuration = new BmPlatformConfiguration();
// ...
configuration.setAttributeSerializers(List.of(new PasswordHashAttributeSerializer(),
                                              new AddressAttributeSerializer()));
     
BmPlatform bmPlatform = BmPlatform.createPlatform(configuration);

Реализуйте метод для установки адреса:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public void setUserAddress(IBmNamespace ns, long userId, String street, String street2, String city, String state, String zipCode)
{
    IBmPlatformTransaction transaction = bmPlatform.beginReadWriteTransaction();
   
    try
    {
        User user = (User)transaction.getObjectById(userId);
        assert user != null : "Invalid user ID";
  
        user.setAddress(new Address(street, street2, city, state, zipCode));
    }
    catch (Throwable t)
    {
        transaction.rollback();
        throw t;
    }
   
    transaction.commit();
}

Теперь объект Address будет храниться вместе с объектом User, которому он принадлежит. Таким образом, вы избежите расходов на хранение отдельного объекта и записи в индексе.

Однако необходимо помнить, что обратной стороной такой экономии является то, что при чтении объекта User всегда будет считываться и объект Address, даже при выполнении с объектом User тех операций, когда его адрес не требуется. Таким образом, этот подход стоит использовать только в тех случаях, когда объект является небольшим или используется очень часто.

Бинарные объекты #

БМ помимо обычных объектов позволяет также хранить неструктурированные (с точки зрения БМ) бинарные объекты, обычно обозначаемые термином BLOB (Binary Large Object). Для работы с бинарными объектами интерфейс IBmPlatformTransaction предоставляет следующие методы:

  • IBmBlob createBlob(IBmNamespace namespace, String fqn) — создает бинарный объект.
  • IBmBlob getBlob(IBmNamespace namespace, String fqn) — получает бинарный объект.
  • void removeBlob(IBmBlob blob) — удаляет бинарный объект.
  • void renameBlob(IBmBlob blob, String newFqn) — переименовывает бинарный объект.

В свою очередь интерфейс IBmBlob предоставляет следующие методы:

  • IBmNamespace getNamespace() — получает пространство имен, которому принадлежит бинарный объект.
  • String getType() — получает тип содержимого бинарного объекта.
  • InputStream getContent() — получает содержимое бинарного объекта.
  • long getLength() — получает длину содержимого бинарного объекта.
  • void setContent(String type, InputStream in) — устанавливает тип и содержимое бинарного объекта.

Сходства и различия бинарных и обычных объектов:

  • Бинарным объектам, как и обычным объектам верхнего уровня, назначается FQN; но, в отличие от обычных объектов, это единственный их идентификатор.
  • Как и в случае с обычными объектами, при работе с бинарными объектами на них накладываются блокировки.
  • Как и в случае с обычными объектами, попытка наложения блокировки может привести к выбросу BmDeadlockDetectedException или BmLockWaitTimeoutException.
  • Как и в случае с обычными объектами, не рекомендуется работать с бинарными объектами вне транзакции.
  • При получении бинарного объекта в локальном кэше транзакции размещается только мета-информация, но не содержимое.
  • Механизм для выброса бинарных объектов из локального кэша транзакции в БМ не предусмотрен.
  • Получение и установка содержимого бинарных объектов работает существенно медленнее, чем получение и установка значений полей обычных объектов.
  • Бинарные объекты следует использовать для размещения данных большого размера (например, картинок), не требующих быстрого доступа.
  • Обычные объекты, напротив, неэффективно использовать для размещения данных большого размера.

Ссылки на внешние объекты #

В БМ существует возможность установки объектам ссылок на EMF-объекты, не хранящиеся в БМ, а также возможность прозрачного разрешения этих ссылок. Наличие этой возможности обеспечивает практически бесшовное взаимодействие с объектами из пакета Ecore, платформенными объектами, BSL-модулями и тому подобными сущностями.

Основные моменты, которые необходимо понимать при работе со ссылками на внешние объекты, заключаются в следующем:

  • Установку и разрешение ссылок на объекты из пакета Ecore БМ осуществляет автоматически.
  • При установке ссылки на внешний объект, не принадлежащий пакету Ecore, сначала производится попытка создания внутреннего представления значения ссылки с помощью механизма IBmReferencePersistenceContributor (см. настройку referencePersistenceContributors в BmPlatformConfiguration). При этом производится перебор IBmReferencePersistenceContributor и у каждого осуществляется вызов метода создания внутреннего представления значения ссылки createReferenceValue(EObject, IBmNamespace) до тех пор, пока не вернется ненулевое значение. Далее вся работа с внутренним представлением значения ссылки (разрешение, сериализация и десериализация) делегируется создавшему ее IBmReferencePersistenceContributor.
  • Если все IBmReferencePersistenceContributorвернули нулевое значение, то в качестве внутреннего представления значения ссылки используется URI целевого объекта. URI получается путем вызова EcoreUtil.getURI(EObject). Чтобы этот метод отработал корректно, целевой объект должен быть привязан к ресурсу, и должна быть обеспечена возможность построения фрагмента для этого объекта в ресурсе. При разрешении URI внешнего объекта используются IBmExternalUriResolver (см. настройку externalUriResolvers в BmPlatformConfiguration). Сначала по URI выбирается подходящий экземпляр IBmExternalUriResolver (см. метод IBmExternalUriResolver.supports(URI)). После этого разрешение целевого объекта делегируется методу IBmExternalUriResolver.getObject(URI).
  • Если метод EcoreUtil.getURI(EObject) возвратил null, то сохранение ссылки на внешний объект невозможно.

Поддержка EMF API #

Важно помнить, что БМ не является полноценной реализацией EMF API. БМ — это высокопроизводительная транзакционная объектная СУБД. На определенном уровне БМ поддерживает совместимость с EMF с целью обеспечения более или менее безболезненной миграции решений, использующих EMF. Поэтому EMF API поддерживается лишь на уровне, минимально необходимом для функционирования решений, реализованных в ранних версиях 1C:EDT.

Далее перечислены основные аспекты, затрагивающие вопрос совместимости БМ с EMF:

Ресурсы #

БМ не оперирует понятиями ресурсов, а оперирует только объектами. При этом, если у объекта, подключенного к БМ, вызвать метод eResource, то будет возвращен некоторый синтетический экземпляр IBmResource, у которого в коллекции contents размещен только лишь соответствующий объект верхнего уровня. В первую очередь метод eResource был реализован для обеспечения корректной работы утилитарного метода EcoreUtil.getURI, который внутри производит конкатенацию URI ресурса с фрагментом.

Основные особенности реализации IBmResource:

  • getContents — возвращает немодифицируемый список, содержащий только один объект верхнего уровня.
  • getURI — возвращает URI со схемой bm и FQN соответствующего объекта верхнего уровня в иерархической части.
  • getResourceSet — возвращает синтетический экземпляр IBmResourceSet.
  • getURIFragment — реализован без каких-либо особенностей.
  • getEObject — реализован без каких-либо особенностей.
  • Все остальные методы не поддерживаются и выбрасывают исключение, либо, если было замечено, что они вызываются в коде 1C:EDT или Xtext, то не делают ничего и производят возврат значений null, false, emptyList и т. п. в зависимости от ситуации.

Особенности реализации IBmResourceSet:

  • IBmResourceSet — создается на каждую транзакцию. Кроме того, есть один глобальный экземпляр, в котором размещаются ресурсы объектов, не привязанные к транзакции.
  • Ресурс объекта БМ не может быть перепривязан к другому экземпляру IBmResourceSet.
  • Коллекция resources — не содержит ресурсы БМ, а содержит только те ресурсы, которые были туда добавлены вами. Это было реализовано для поддержки предыдущих решений по внутреннему экспорту для обеспечения возможности разрешения ссылок на объекты БМ из объектов, не хранящихся в БМ.
  • Остальные методы либо не реализованы, либо наследуют дефолтные реализации ResourceSetImpl, либо оставлены пустыми.

В заключение следует сказать, что при разработке механизмов на базе БМ не следует пользоваться IBmResource и IBmResourceSet, так как:

  • Во-первых, как уже говорилось, данная функциональность была реализована на минимальном уровне лишь для обеспечения работоспособности механизмов, реализованных в ранних версиях 1C:EDT, соответственно, ее развитие не планируется.
  • Во-вторых, работа через IBmResource и IBmResourceSet существенно менее эффективна с точки зрения производительности, чем работа с API БМ.

Транзиентные features #

БМ игнорирует модификатор transient, таким образом features, помеченные данным модификатором, все равно записываются в хранилище.

Поддержка старого API #

До появления версии 1С:EDT 2023.1 в БМ отсутствовало понятие пространства имен. Поэтому с целью разделения объектов, принадлежащих разным проектам, на каждый проект создавался отдельный экземпляр БМ, представляемый объектом IBmEngine. Подобно тому, как в текущей версии любое взаимодействие с платформой начинается с вызова одного из методов объекта BmPlatform, в предыдущих версиях взаимодействие начиналось с вызова одного из методов объекта IBmEngine, как правило, с метода открытия транзакции. Для работы с транзакцией в предыдущих версиях использовался объект IBmTransaction, набор методов которого аналогичен набору методов IBmPlatformTransaction, с той лишь разницей, что практически каждый метод IBmPlatformTransaction принимает в качестве одного из параметров объект IBmNamespace.

Однако старое решение было неудобно тем, что не позволяло в одной транзакции работать с данными нескольких проектов, а также было далеко не оптимально с точки зрения потребления ресурсов (например, каждая инстанция БМ захватывала необходимое ей количество оперативной памяти для хранения индексов, соответственно потребление памяти в 1С:EDT росло кратно количеству проектов, открытых в рабочей области). Для адресации указанных проблем и была реализована текущая платформа БМ с поддержкой пространств имен.

Тем не менее, в текущей версии было принято решение не избавляться от интерфейсов IBmEngine и IBmTransaction, чтобы упростить миграцию существующих решений на новый API. В текущем решении эти объекты являются проекциями объектов BmPlatform и IBmPlatformTransaction, привязанными к определенному пространству имен. Так, интерфейс IBmNamespace предоставляет метод IBmEngine asEngine(), а интерфейс IBmPlatformTransaction предоставляет метод IBmTransaction getNamespaceBoundTransaction(IBmNamespace).

Для начала рассмотрим метод getNamespaceBoundTransaction(IBmNamespace) интерфейса IBmPlatformTransaction. Он возвращает реализацию IBmTransaction, которая представляет не новую транзакцию, а всего лишь обертку над IBmPlatformTransaction. Эта обертка делегирует выполнение всех действий исходного экземпляра IBmPlatformTransaction. При этом в те методы IBmPlatformTransaction, которые принимают пространство имен, передается пространство имен, переданное в качестве параметра в метод getNamespaceBoundTransaction. Ниже приведена эквивалентная реализация IBmTransaction:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public final BmTransactionImpl implements IBmTransaction
{
    private final IBmPlatformTransaction platformTransaction;
    private final IBmNamespace namespace;
 
    public BmTransactionImpl(IBmPlatformTransaction platformTransaction, IBmNamespace namespace)
    {
        this.platformTransaction = platformTransaction;
        this.namespace = namespace;
    }
 
    @Override
    public IBmTransactionCommitResult commit()
    {
        return platformTransaction.commit();
    }
 
    @Override
    public void rollback()
    {
        platformTransaction.rollback();
    }
 
    @Override
    public void attachTopObject(IBmObject object, String fqn)
    {
        platformTransaction.attachTopObject(namespace, object, fqn);
    }
 
    @Override
    public void detachTopObject(IBmObject object)
    {
        platformTransaction.detachTopObject(object);
    }
 
    @Override
    public IBmObject getObjectById(long id)
    {
        return platformTransaction.getObjectById(namespace, id);
    }
 
    // ...
}

Соответственно, метод asEngine() интерфейса IBmNamespace возвращает реализацию IBmEngine, которая представляет собой обертку над BmPlatform. Все методы этой обертки также работают в контексте пространства имен, к которому обертка привязана. Ниже приведена эквивалентная реализация IBmEngine.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public final BmEngineImpl implements IBmEngine
{
    private final BmPlatform platform;
    private final IBmNamespace namespace;
  
    public BmEngineImpl(BmPlatform platform, IBmNamespace namespace)
    {
        this.platform = platform;
        this.namespace = namespace;
    }
  
    @Override
    public String getId()
    {
        return namespace.getName();
    }
 
    @Override
    public IBmTransaction getCurrentTransaction()
    {
        IBmPlatformTransaction platformTransaction = platform.getCurrentTransaction();
        if (platformTransaction == null)
        {
            return null;
        }
 
        return platformTransaction.getNamespaceBoundTransaction(namespace);
    }
 
    @Override
    public IBmTransaction beginReadWriteTransaction()
    {
        IBmPlatformTransaction platformTransaction = platform.beginReadWriteTransaction();
        return platformTransaction.getNamespaceBoundTransaction(namespace);
    }
 
    // ...
}

Для перехода от старого API к новому интерфейс IBmEngine предоставляет метод IBmNamespace asNamespace(), возвращающий пространство имен, к которому привязана текущая инстанция. Аналогично, интерфейс IBmTransaction для получения связанных с ним транзакции и пространства имен предоставляет методы IBmPlatformTransaction getPlatformTransaction() и IBmNamespace getNamespace().

При работе со старым API необходимо помнить, что IBmTransaction это всего лишь обертка-адаптер интерфейса IBmPlatformTransaction, а не некий специфичный вид транзакции. Соответственно, все экземпляры IBmTransaction, полученные в одном потоке, представляют одну транзакцию, а не разные. Рассмотрите следующий пример. Предположим, у вас есть реализованный на базе старого API метод для переноса задачи из одного проекта в другой:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void moveTask(IBmEngine sourceProjectEngine, IBmEngine targetProjectEngine, long sourceTaskId)
{
    IBmTransaction sourceProjectTransaction = sourceProjectEngine.beginReadWriteTransaction();
    try
    {
        Task sourceTask = (Task)sourceProjectTransaction.getObjectById(sourceTaskId);
        assert sourceTask != null : "Invalid task ID";
         
        IBmTransaction targetProjectTransaction = targetProjectEngine.beginReadWriteTransaction();
        try
        {
            createTaskCopy(targetProjectTransaction, sourceTask);
        }
        catch (Throwable t)
        {
            targetProjectTransaction.rollback();
            throw t;
        }
 
        targetProjectTransaction.commit();
 
        sourceProjectTransaction.detachTopObject(sourceTask);
    }
    catch (Throwable t)
    {
        sourceProjectTransaction.rollback();
        throw t;
    }
 
    sourceProjectTransaction.commit();
}

Хотя выглядит данный метод практически не читаемо, в старом решении он был бы вполне работоспособен, если не считать, что он не обеспечивал бы атомарность изменений, да еще мог бы привести к неразрешимым средствами БМ ситуациям взаимной блокировки. В новом же решении при попытке создать вторую транзакцию в стр. 9 будет выброшено исключение, т. к. транзакция уже открыта в стр. 3.

Чтобы заставить данный код работать в новом решении, не прибегая к полному переводу на новый API, вы можете изменить его следующим образом:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public void moveTask(IBmEngine sourceProjectEngine, IBmEngine targetProjectEngine, long sourceTaskId)
{
    IBmTransaction sourceProjectTransaction = sourceProjectEngine.beginReadWriteTransaction();
    try
    {
        Task sourceTask = (Task)sourceProjectTransaction.getObjectById(sourceTaskId);
        assert sourceTask != null : "Invalid task ID";
         
        IBmTransaction targetProjectTransaction = targetProjectEngine.getCurrentTransaction();
        createTaskCopy(targetProjectTransaction, sourceTask);
 
        sourceProjectTransaction.detachTopObject(sourceTask);
    }
    catch (Throwable t)
    {
        sourceProjectTransaction.rollback();
        throw t;
    }
 
    sourceProjectTransaction.commit();
}

Заметьте, что операции фиксации и отката выполняются один раз посредством вызова соответствующих методов на объекте sourceProjectTransaction, так как, хотя оберток две, транзакция одна. За счет этого приведенный метод будет выполняться атомарно и не будет приводить к неразрешимым ситуациям взаимной блокировки.

Однако, во избежание введения в заблуждение тех, кто будет работать с вашим кодом, все же рекомендуется все компоненты, осуществляющие одновременную работу с несколькими пространствами имен, по возможности переводить на использование нового API. Обратите внимание, что приведенная ниже версия этого же метода, реализованная на базе нового API, выглядит логичнее:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public void moveTask(IBmNamespace sourceProjectNamespace, IBmNamespace targetProjectNamespace, long sourceTaskId)
{
    IBmPlatformTransaction transaction = platform.beginReadWriteTransaction();
    try
    {
        Task sourceTask = (Task)transaction.getObjectById(sourceProjectNamespace, sourceTaskId);
        assert sourceTask != null : "Invalid task ID";
         
        createTaskCopy(transaction, targetProjectNamespace, sourceTask);
 
        transaction.detachTopObject(sourceTask);
    }
    catch (Throwable t)
    {
        transaction.rollback();
        throw t;
    }
 
    transaction.commit();
}