Реализация пагинации (последовательная обработка данных)

Итеративная обработка данных

В Серверном API нет методов, реализующих пагинацию, как, например, в GET-запросах Table API. Без пагинации, при выборке большого количества записей из таблицы, мы можем столкнуться с ограничением на размер памяти ОЗУ, выделяемого на выполнение серверного скрипта.
Для решения данной проблемы можно разделить обработку данных на “порции” и создать скрипт по расписанию, который будет последовательно обрабатывать все записи в выборке. Для этого:

  1. Создайте неактивный скрипт по расписанию с аналогичными настройками:

  2. Укажите содержимое в скрипте:

    (function executeScheduleScript() {
    	let lastRecordId = '1';
    	const MESSAGE_PREFIX = 'Last record: ';
    	const PROCESSING_LIMIT = 10;
    	const TABLE_NAME = 'sys_translation';
    	const CONDITION = '(application_id=155931135900000002)';
    
    	const fiveMinutesAgo = new SimpleDateTime();
    	fiveMinutesAgo.addSeconds(-600);
    
    	const logMessage = new SimpleRecord('sys_log');
    	logMessage.addQuery('level', 'debug');
    	logMessage.addQuery('message', 'STARTSWITH', MESSAGE_PREFIX);
    	logMessage.addQuery('sys_created_at', '>', fiveMinutesAgo.getValue());
    	logMessage.orderByDesc('sys_created_at');
    	logMessage.selectAttributes('message');
    	logMessage.setLimit(1);
    	logMessage.query();
    
    	if (logMessage.next() && logMessage.message.match(/\d+/g)) {
    		lastRecordId = logMessage.message.match(/\d+/g)[0];
    	}
    
    	function processRecords() {
    		const record = new SimpleRecord(TABLE_NAME);
    		record.addEncodedQuery(CONDITION);
    		record.addQuery('sys_id', '>', lastRecordId);
            record.orderBy('sys_id');
    		record.setLimit(PROCESSING_LIMIT);
    		record.query();
    
    		while (record.next()) {
                lastRecordId = record.sys_id;
    
    			// ... processing actions
    		}
    
    		ss.debug(`${MESSAGE_PREFIX}${lastRecordId}`);
    	}
    
    	processRecords();
    })();
    
    
  3. Укажите нужные таблицу и условие выборки в переменные TABLE_NAME и CONDITION

  4. Укажите скрипт, который будет обрабатывать записи вместо строки // ... processing actions

  5. Активируйте скрипт по расписанию

  6. Отслеживайте записи sys_log с источником Debug /list/sys_log?condition=level=debug

  7. Скорректируйте значение PROCESSING_LIMIT в скрипте по расписанию, чтобы скрипт успевал обработать выборку до следующего запуска.

Постраничный доступ к данным

На основе представленной логики, можно организовать постраничную выборку данных, например, через функцию getNextPage, которая реализует одну итерацию пагинации (одну “страницу” данных) из таблицы, задаваемой через параметр tableName, начиная с записи после startRecordId. Начальное значение startRecordId должно быть равно максимально возможному sys_id, чтобы начать с новых записей, в противном случае на первой “странице” будут доступны наиболее старые записи.

function getNextPage(tableName, limit = 10, startRecordId = 'ffffffffffffffffffffffffffffffff') {
    const record = new SimpleRecord(tableName);
    record.addQuery('sys_id', '<', startRecordId);
    record.orderByDesc('sys_id');
    record.setLimit(limit);
    record.selectAttributes(['sys_id', 'number']);
    record.query();
    
    const data = [];
    let lastRecordId = startRecordId;
    while (record.next()) {
        lastRecordId = record.getValue('sys_id');
        data.push(record.getAttributes());
    }

    return { data, lastRecordId }; // {"data":[],"lastRecordId":""}
}

Если нужно получить n-ую страницу, можно сделать обертку в цикл:

function getPage(tableName, pageNumber, pageSize = 10) {
    let lastRecordId = 'ffffffffffffffffffffffffffffffff';
    let currentPage = 1;
    let page;

    while (currentPage <= pageNumber) {
        page = getNextPage(tableName, pageSize, lastRecordId);
        lastRecordId = page.lastRecordId;
        currentPage++;
    }

    return page; // {"data":[],"lastRecordId":""}
}

Важно заметить, что функция getPage неэффективна при больших значениях pageNumber, т.к. при попытке получить, например, 100-ю страницу, функция вызывает getNextPage 100 раз. Это не критично при малых объёмах, но сильно влияет на производительность при больших значениях pageNumber.

Table API

Проблему производительности можно решить воспользовавшись Table API и SimpleRestRequest. С помощью SimpleWebService sws.restRequestV1() создается объект SimpleRestRequest и далее с помощью его методов setRequestMethod, setRequestUrl и прочих, формируется запрос к Table API:

function getPage(tableName, pageNumber = 1, pageSize = 10) {
    const request = sws.restRequestV1();
    request.setRequestMethod('GET');
    const instanceUri = ss.getProperty('simple.instance.uri');
    request.setRequestUrl(`https://${instanceUri}/rest/v1/table/${tableName}`);
    request.setQueryParameter('sysparm_fields', 'number,sys_id');
    request.setRequestHeader('Authorization', `Bearer XXXXXXXX`);
    request.setQueryParameter('sysparm_page', pageNumber.toString());
    request.setQueryParameter('sysparm_limit', pageSize.toString());
    const response = JSON.parse(request.execute().getBody());
    return response; // {"status":"OK","data":[]}
}

Важно: Table API требует авторизации, поддерживаются Basic Auth и Bearer Token.

6 лайков