...

Миграция исторических данных в ELMA365 SaaS

Тема в разделе "Примеры решений и дополнительных модулей", создана пользователем kamyshev, 20 дек 2021.

  1. kamyshev

    kamyshev Активный участник

    Часто во время проектов внедрения заходит вопрос о миграции исторических данных из старых систем клиента в наш продукт ELMA365. И если в случае выбора On-Premises решения есть понимание, как подступится к задаче, то в случае SaaS решения не всегда понятно, как же выполнить эту миграцию. Особенно, когда нужно перенести не только метаданные (значения атрибутов), но и перенести файлы (допустим, версии документов).

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

    Весь процесс миграции можно разбить на несколько этапов:
    • Подготовка инфраструктуры для переноса файлов.
    • Подготовка атрибутивной модели Приложений для импорта метаданных.
    • Подготовка самих метаданных.
    • Моделирование бизнес-процесса и написание сценариев для импорт данных и "маппинга" атрибутов.
    Подготовка инфраструктуры для переноса файлов

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

    Самый простой вариант развернуть HTTP-сервер Apache. В рамках статьи вопрос установки данного решения рассматривать не буду, в Интернете много статей про установку под различные ОС.

    Для удобства и наглядности процесса загрузки файлов на сервер можно установить любое Open source решение для этих целей, например Nextcloud, который доступен через менеджер пакетов snap.

    При работе с методом fetch у нас, к сожалению, нет возможности получать содержимое каталогов. Для выхода из данной ситуации можно написать bash-скрипт или bat-файл (Windows). Пример bash-сценария:
    Код:
    
    #!/bin/bash

    IFS=$'\n'
    for file in $(find $PWD -type d)
    do
    cd $file
    find 
    . -type f -printf '%f\n' desc.txt
    cd 
    ..
    done desc.txt
    Данный скрипт рекурсивно обходит все вложенные каталоги и внутри каждого создает файл desc.txt со списком всех файлов в данном каталоге. Далее, в сценарии мы сможем получать содержимое данного файла и превращать в массив названий файлов и выполнять обход по нему.

    Подготовка атрибутивной модели Приложений для импорта метаданных

    При миграции метаданных возможно 2 ситуации:
    1. Все данные для нашего Приложения в ELMA365 хранятся в атрибутах самого Приложения
    2. Данные для нашего Приложения, для которого осуществляем миграцию данных, хранятся в различных Приложения-справочниках. Сами атрибуты представлены в виде контекстных переменных типа Приложения.
    В первом случае всё достаточно просто: используем механизм импорта данных по шаблону (Excel или CSV). Всё подробно описано в справке.

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

    Подготовка метаданных

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

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

    Моделирование бизнес-процесса и написание сценариев для импорт данных и "маппинга" атрибутов

    Бизнес-процесс и сценарии в нём берут на себя задачи по "вытягиванию" файлов с сервера и "маппинг" атрибутов, если данные необходимо хранить в Приложениях-справочниках. При проектирование БП всегда следует помнить про ограничения в облаке: 99 итераций цикла в БП (можно на 100-й итерации включать в процесс таймер на 1 минуту в качестве брейкпоинта) и ограничение на выборку в 10 тыс. элементов и на размер пакета gRPC. Данные ограничения можно обойти использовав 2 цикла в БП. На большом круге будет формироваться скоуп из обрабатываемых элементов, на малом круге будет происходить работа с одним элементом из выборке.

    upload_2021-12-20_12-14-46.png

    Спойлер: Листинг сценариев
    Код:
    
    // Адрес Apache-сервера, где лежат файлы для загрузки
    const url_address 'http://files-ubuntu.vps.elewise.com:8080'
    // Размер постраничной выборки элементов приложения
    const page_size 90

    async 
    function initData(): Promise<void> {
        if (
    Context.data.contracts_quantity == 0) {
            
    Context.data.contracts_quantity await Context.fields.contract.app.search().where(=> f.__deletedAt.eq(null)).size(page_size).count()
        }
        
    Context.data.contract await Context.fields.contract.app.search().where(=> f.__deletedAt.eq(null)).from(Context.data.contracts_pages_counter!).size(page_size).all()
        
    Context.data.counter Context.data.contract.length
        Context
    .data.contracts_pages_counter! += page_size
    }

    async function downloadContracts(): Promise<void> {
        const 
    Context.data.counter! - 1
        
    const contract await Context.data.contract![i].fetch()
        if (
    contract.data.isHasHistoryFiles || contract.data.historyId) {
            const 
    file_dir contract!.data.historyId!
            
    // Обращаемся к файлу, который содержит имена файлов в каталоге
            
    const req await fetch(encodeURI(`${url_address}/contracts/ДОГ${file_dir}/desc.txt`))
            if (
    req.ok) {
                
    // Формируем массив из имён файлов
                
    const str_array = (await req.text()).split('\n')
                const 
    file_arrayPromise<FileItem>[] = []
                for (
    let item of str_array) {
                    if (
    item && item !== 'desc.txt') {
                        
    // Формируем массив промисов
                        
    file_array.push(contract.fields.doc_files.createFromLink(itemencodeURI(`${url_address}/contracts/ДОГ${file_dir}/${item}`)))
                    }
                }
                
    contract.data.doc_files await Promise.all(file_array)
                
    await contract.save()
            }
            else {
                
    Context.data.debug += `${req.status}${req.statusText}:
                 
    ${contract.data.__name} ${contract.data.historyId}
                 
    ${req.url} `
            }
        }
        
    Context.data.counter!--
        
    updateData(contract.data.__id)
    }

    async function updateData(itemstring): Promise<void> {
        
    // Матчим данные из справочников
        
    const document await Context.fields.contract.app.search().where(=> f.__id.eq(item)).first()
        if (
    document) {
            
    document.data.ourCompany2 await document.fields.ourCompany2.app.search().where(=> f.id.eq(document.data.migration_ourCompany!)).first()
            
    document.data.contractType await document.fields.contractType.app.search().where(=> f.code.eq(document.data.migration_contractType!)).first()
            
    document.data.subdivision await document.fields.subdivision.app.search().where(=> f.id.eq(document.data.migration_subdivision!)).first()
            
    document.data.budgetLine await document.fields.budgetLine.app.search().where(=> f.__name.like(document.data.migration_budgetLine!)).first()
            
    document.data.status await document.fields.status.app.search().where(=> f.__name.like(document.data.migration_status!)).first()
            
    document.data.historicalDocument true;
            
    await document.save()
            const 
    history_group await System.userGroups.search().where((fg) => g.and(f.__name.like('АРХД'))).first()
            
    await document.setPermissions(new Permissions([new PermissionValue(history_group!, [PermissionType.READ])]))
            
    await document.setStatus(document.fields.__status.variants.registered)
        }
    }

    Придерживаясь основных принципов из этой статьи, можно осуществить импорт данных практических из любой системы, включая файлы. Конкретная реализация всегда будет зависеть от конкретного проекта и legacy-системы заказчика. По импорту больших файлов недавно обновился сам механизм, о нём можно прочитать в последних release-notes::TEAM-5688.

    Пример реализации переноса данных из MS Dynamics можно посмотреть здесь.
    Последнее редактирование: 14 апр 2022
  2. f.nikolaev

    f.nikolaev Участник

    Рекомендую ставить таймер на 2 минуты.
    Связано с тем, что блок таймера сравнивает значения минут, не учитывая значения секунд. При установке таймера на 1 минуту задержка будет составлять от 1 до 59 секунд, в зависимости от текущего времени его запуска, ошибка по количеству циклов в этом случае срабатывает.
    При установке таймера на 2 минуты задержка соответственно будет от 1мин 1с до 1мин 59с, ошибки по количеству циклов не будет.