Kotlin/Java Backend

Для работы с RecordsAPI на kotlin/java бэкенде предусмотрена библиотека ecos-records - https://github.com/Citeck/ecos-records

Подключив библиотеку можно создать RecordsServiceFactory и получить оттуда все сервисы для работы с RecordsAPI. Инициализация сервисов инкапсулирована в RecordsServiceFactory и не требует обязательного наличия DI механизмов.

Основной сервис для работы с RecordsAPI - ru.citeck.ecos.records3.RecordsService. Пример использования:

val serviceFactory = RecordsServiceFactory()
val recordsService = serviceFactory.recordsServiceV1

val value = HashMap<String, String>()
value["someKey"] = "someValue"

val attributeValue = recordsService.getAtt(value, "someKey").asText()
println(attributeValue) // someValue

Здесь мы создаем новую мапу с одним значением и получаем из неё атрибут с именем someKey через RecordsService.

Есть два основных сценария использования RecordsService:

  • Работа с уже готовыми данными как в примере выше. Нам не нужно никуда отправлять запросы и получение атрибутов проходит в пределах сервиса. В этом режиме доступно только получение атрибутов и Records DAO никак не задействуются.

  • Работа с ссылками (EntityRef). В этом режиме сервис взаимодействует с источниками данных, функционал которых реализован через следующие интерфейсы:

    • RecordsDao базовый интерфейс для всех остальных ниже по списку. Содержит только один метод - String getId(), который используется при регистрации RecordsDao в RecordsService;

    • RecordsQueryDao для поиска записей;

    • RecordsAttsDao (RecordAttsDao) для получения атрибутов по заранее известным идентификаторам записей;

    • RecordMutateDao для создания или редактирования записей;

    • RecordsDeleteDao (RecordDeleteDao) для удаления записей;

Примечание

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

Records DAO - реализация абстрактного понятия «Источник данных». Один Records DAO может представлять разные источники данных.

При работе с Records DAO в зависимости от типа действия происходит следующее:

  • Query. Мы передаем в RecordsQueryDao поисковый запрос и ждем на выходе следующие типы значений (поддерживаются как коллекции этих значений так и значения в одном экземпляре):

    • EntityRef - ссылки на сущности. Если мы получаем ссылки, то сервис обращается к соответствующему RecordsAttsDao для получения атрибутов;

    • String - текстовый результат означает что мы вернули идентификаторы записей, по которым нам нужно получить атрибуты через RecordsAttsDao. Если в строке не указан другой Records DAO, то используется тот же, у которого мы вызывали query;

    • RecsQueryRes - список записей вместе с данными об их общем количестве;

    • Any - любое другое значение, которое обрабатывается с использованием реализаций интерфейса AttValueFactory;

  • Get attributes. Получение атрибутов по идентификаторам записей. Этот метод используется либо с результатом Query из предыдущего пункта либо посредством прямого вызова recordsService.getAtts(...) Метод возвращает любое значение, которое обрабатывается с использованием реализаций интерфейса AttValueFactory;

  • Mutate. Изменение или создание записей через RecordMutateDao

    В Records API создание записи происходит при мутации записи с пустым локальным идентификатором. Т.е. если мы хотим создать сущность в микросервисе emodel в источнике данных types-repo то делаем следующее:

    // здесь следует обратить внимание на строку 'emodel/types-repo@'.
    // Согласно структуре RecordRef'а (ссылка внизу) здесь
    // (AppName - "emodel", SourceId - "types-repo", LocalId - "" (пустая строка))
    let newRecord = Records.get('emodel/types-repo@');
    
    newRecord.att("id", "id-value");
    newRecord.att("name", "Custom name");
    // В resultRecord будет созданная запись.
    // Если мы задаем id вручную (как двумя строчками выше),
    // то в resultRecord будет лежать то же самое что мы получим при выполнении
    // Records.get('emodel/types-repo@id-value');
    // Если мы не указали вручную ID, то он сгенерируется в виде UUID.
    let resultRecord = await newRecord.save();
    
  • Delete. Удаление записей через RecordsDeleteDao

AttValue - интерфейс, который представляет собой значение, с которым умеет работать RecordsService при получении атрибутов. Методы интерфейса:

Promise<?> init() // инициализация значения перед тем как начать вычисление атрибутов
Object getId() // идентификатор значения. Может быть как строкой, так и EntityRef
Object getDisplayName() // значение для скаляра "?disp"
String asText() // значение для скаляра "?str"
Object getAs(String type) // значение для спец. атрибута "_as"
Double asDouble() // значение для скаляра "?num"
Boolean asBoolean() // значение для скаляра "?bool"
Object asJson() // значение для скаляра "?json"
Object asRaw() // значение для скаляра "?raw"
Object asBin() // значение для скаляра "?bin"
has(String name) // значение для спец. атрибута "_has"
Object getAtt(String name) // получить значение атрибута по его имени
AttEdge getEdge(String name) // получить мета-информацию об атрибуте по его имени
Object getType() // получить ECOS тип значения

AttValueFactory - интерфейс для преобразования произвольных типов данных в имплементацию AttValue

// Проинициализировать фабрику. В основном используется для получения конвертеров для других типов.
// Например: attValuesConverter.getFactory(DataValueAttFactory.class)
void init(attValuesConverter: AttValuesConverter)

// Получить реализацию AttValue для значения
AttValue getValue(T value)

// Получить список доступных типов значений, которые может обрабатывать данная фабрика
List<Class<*>> getValueTypes()

// Получить приоритет фабрики. Чем выше приоритет, тем важнее фабрика в случае если для одного и того же типа нашлось две фабрики.
int getPriority()

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

resources/META-INF/services/ru.citeck.ecos.records3.record.atts.value.factory.AttValueFactory

Внутри этого файла должно быть полное имя класса (вместе с пакетом) с вашей реализацией интерфейса AttValueFactory

Пример: https://github.com/Citeck/ecos-records/blob/master/ecos-records/src/test/resources/META-INF/services/ru.citeck.ecos.records3.record.atts.value.factory.AttValueFactory

Если для значения не нашлось подходящего AttValueFactory, то используется стандартная фабрика BeanValueFactory. Эта фабрика работает со значением как с бином, у которого ищутся геттеры для атрибутов.

Например, если у нас есть следующий бин:

static class TestDto {
  private String field;
  void setField(String value) {
    this.field = value;
  }
  String getField() {
    return field;
  }
}

То с точки зрения BeanValueFactory у этого бина есть значение с одним атрибутом «field». Пример работы:

RecordsServiceFactory serviceFactory = new RecordsServiceFactory();
RecordsService recordsService = serviceFactory.getRecordsServiceV1();

TestDto value = new TestDto();
value.setField("field-value");

String attributeValue = recordsService.getAtt(value, "someKey").asText();
System.out.println(attributeValue); // field-value

Если же мы хотим изменить имя атрибута не меняя названия методов, то можно воспользоваться аннотацией AttName:

static class TestDto {
  private String field;
  void setField(String value) {
    this.field = value;
  }
  @AttName("otherName")
  String getField() {
    return field;
  }
}
...
TestDto value = new TestDto();
value.setField("field-value-2");

String attributeValue = recordsService.getAtt(value, "otherName").asText();
System.out.println(attributeValue); // field-value-2

Аннотация @AttName помогает задать произвольное имя атрибута. Её можно использовать:

  • На геттере, чтобы дать произвольное название атрибуту;

  • На сеттере для конвертации DTO -> Схема атрибутов для запроса; (см. методы recordsService.getAtts(Any record, Class<?> atts))

  • Аннотация на поле работает как для сеттера так и для геттера если они есть;

Аннотация @AttName может в качестве аргумента принимать значение "...". Такая запись означает, что все атрибуты из поля с этой аннотацией будут доступны так же и в нашем значении. Пример:

static class ParentDto {
  @AttName("...")
  private ChildDto child = new ChildDto(); // опустим сеттер, чтобы не усложнять пример
  public ChildDto getChild() {
    return child;
  }
}
static class ChildDto {
   public String getValue(): String {
     return "abc"; // геттер не обязательно должен отдавать значение поля. Его поведение может быть произвольным
   }
}
...
ParentDto value = new ParentDto();

// Если бы аннотация AttName отсутствовала, то до значения 'abc' мы бы могли добраться так:
// recordsService.getAtt(value, "child.value").asText();
// Но с аннотацией @AttValue("...") можно обращаться к вложенному атрибуту так:

String attributeValue = recordsService.getAtt(value, "value").asText();
System.out.println(attributeValue); // abc

Так же особое значение имеют аннотации AttName где в качестве аргумента указан один из скаляров с вопросительным знаком. Например: @AttName("?str"). Такие геттеры вызываются при загрузке скаляров.

BeanValueFactory так же ищет в бине ряд специальных методов по их имени и аргументам (тип возвращаемого значения не важен):

Object getId() // значение для скаляра ?id
Object getAsStr() // значение для скаляра "?str"
Object getAsNum() // значение для скаляра "?num"
Object getAsBool() // значение для скаляра "?bool"
Object getAsJson() // значение для скаляра "?json"
Object getAsRaw() // значение для скаляра "?raw"
Object getAsBin() // значение для скаляра "?bin"
Object getEcosType() // значение для атрибута "_type"
Object getAs(String name) // значение для спец. атрибута "_as"
Object has(String name) // значение для спец. атрибута "_has"
Object getEdge(String name) // значение для спец. атрибута "_edge"
Object getAtt(String name) // Значение атрибута по имени если не получилось найти геттер для него

Для отображаемого имени нашего бина BeanValueFactory ищет следующие методы в порядке убывания приоритета (используется первый найденный):

Object getDisplayName()
Object getLabel()
Object getTitle()
Object getName()

Типы данных в Kotlin/Java

Метод ObjectData set/add

set — установить значение по ключу. Аналог метода put в Map.

add — добавить элемент к массиву. Сам по себе ObjectData является аналогом Map, и для него add имеет мало смысла напрямую, но add первым аргументом может принимать путь до вложенного массива.

Например:

ObjectData data = ObjectData.create();
data.set("arr", new LinkedList<Object>());
data.add("$.arr", "new-element");

Формат с $ — стандарт JsonPath.