...

4.1. Примеры поиска с помощью Search

Тема в разделе "Руководство по настройке форм и сценариев", создана пользователем ELMA365, 15 авг 2023.

  1. ELMA365

    ELMA365 Moderator

    Пример 1. Поиск пересечения отпусков.

    Задача: нужно, чтобы сотрудник при указании периода мог сразу увидеть пересечения с коллегами и сдвинуть даты своего отпуска.

    Чтобы найти отпуска, которые попадают в указанный период, нам потребуется воспользоваться поиском.

    Создадим новый виджет и назовём его vacation_overlap. На вход в нём будет приходить выбранный период, а в качестве результата мы будем возвращать список дат отпусков и фамилии сотрудников, которые попали в пересечение.

    Также добавим в контекст виджета переменную типа Приложение со ссылкой на созданное нами приложение Отпуск:

    [​IMG]

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

    Код:
    
    async function onInit(): Promise<void> {
        if (!
    Context.data.first_date || !Context.data.second_date) {
            return;
        }
        
    // Создаём поиск и заполняем условия
        
    const vacation_searcher Context.fields.vacation.app.search();
        
    vacation_searcher.where((vacationgf) =>
            
    // Задаем условие через ИЛИ
            
    gf.or(
                
    // Здесь условие уже через И
                
    gf.and(
                    
    vacation.date_start.gte(Context.data.first_date!),
                    
    vacation.date_start.lt(Context.data.second_date!),
                ),
                
    gf.and(
                    
    vacation.date_end.gt(Context.data.first_date!),
                    
    vacation.date_end.lte(Context.data.second_date!),
                ),
                
    gf.and(
                    
    vacation.date_start.lt(Context.data.first_date!),
                    
    vacation.date_end.gt(Context.data.second_date!),
                )
            )
        );
        
    // Ограничиваем размер получаемой выборки
        
    vacation_searcher.size(100);
        
    // Сортируем по дате создания
        
    vacation_searcher.sort("date_start"false);
        
    // Выполняем запрос и сохраняем результат в переменную
        
    const vacations await vacation_searcher.all();
        
    Context.data.vacations vacations.map(vac => vac.data.__name);
    }
    Context.data.vacations в сценарии — это переменная произвольного типа, которая нужна для передачи массива строк в шаблон.

    На форму шаблона добавим виджет Код со следующим содержимым:
    Код:
    
    <% if (Context.data.vacations) { %>
        <% for(
    let item of Context.data.vacations) { %>
            <
    div><%= item%></div>
        <% } %>
    <% } %>
    Запускаем отладку и проверяем все возможные случаи пересечения. Обязательно проверяем случаи, когда отпуска не пересекаются.

    Когда тестирование успешно окончено, публикуем виджет с комментарием:

    [​IMG]

    Теперь можно добавить виджет на форму создания отпуска. Допишем немного кода в виджет Код, добавленный на неё:
    Код:
    
    <% if (Context.data.date_start || Context.data.date_end) { %>
        <%= 
    UI.widget.groupbox({title'Количество рабочих дней'collapsibletrue }, panelDaysCount) %>
        <%= 
    UI.widget.groupbox({title'Пересечения'collapsibletrue }, panelOverlap) %>
    <% } %>
    <% 
    $template panelDaysCount %>

        <%= 
    UI.widget.render('leave@days_count', {
                
    readonlytrue,
                
    first_dateContext.data.date_start,
                
    second_dateContext.data.date_end,
                
    days_countContext.data.days,
                
    working_hoursViewContext.data.workminutes
        
    } ) %>
    <% 
    $endtemplate %>
    <% 
    $template panelOverlap %>
            <%= 
    UI.widget.render('leave@vacation_overlap', {
                
    readonlytrue,
                
    first_dateContext.data.date_start,
                
    second_dateContext.data.date_end,
        } ) %>
    <% 
    $endtemplate %>
    Получаем следующий результат:

    [​IMG]


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

    Полностью скопируем код из клиентского сценария, изменив название функции с onInit() на getVacationOverlap().

    Функцию onInit перепишем следующим образом:
    Код:
    
    async function onInit(): Promise<void> {
        if (!
    Context.data.first_date || !Context.data.second_date) {
            return;
        }
        
    await Server.rpc.getVacationOverlap();
    }
    Обратите внимание, что проверку на то, заполнены ли поля с датами, мы оставили и на клиентской стороне. Это позволит нам не ждать сервер, если данных ещё нет.

    В серверном коде проверку также оставили: она не влияет на быстродействие, но поможет избежать ошибок.

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

    [​IMG]

    В заключение добавим фильтрацию по статусу Согласован и типу отпуска Основной оплачиваемый:
    Код:
    
    // Только со статусом Согласован
    vacation_searcher.where(vacation => vacation.__status.eq(Context.fields.vacation.app.fields.__status.variants.agreed.id));
    const 
    leave_type_main await Context.fields.vacation_type.app.search()
      .
    where(vt => vt.__name.eq('Основной оплачиваемый'))
      .
    first();
    // Если есть такой вид отпуска, как Основной оплачиваемый
    if (leave_type_main) {
      
    // то ищем только по таким элементам
      // условие добавится через И
      
    vacation_searcher.where(vacation => vacation.leave_type.link(leave_type_main));
    }
    // Выполняем запрос и сохраняем результат в переменную
    const vacations await vacation_searcher.all();
    В итоге с учётом условий осталось только одно пересечение:

    [​IMG]

    Пример 2. Получение списка телефонов контактов из определённых компаний.

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

    Делаем решение «в лоб».

    Для начала получим список компаний с фильтрацией по отрасли. Нужно учесть, что отрасль могут не задать. Тогда нужно найти вообще все компании.
    Код:
    
    /**
    * Получить список компаний, отфильтрованный по полю Отрасль
    * @param limit Размер выборки, по умолчанию - 1000
    * @returns Список компаний
    */
    async function getCompanies(limitnumber 1000): Promise<ApplicationItem<Application$_clients$_companies$Dataany>[]> {
        const 
    companySearch Context.fields.company.app.search();
        if (
    Context.data.industry) {
            
    companySearch.where(it => it._industries.has(Context.data.industry!.id));
        }
        return 
    await companySearch.size(limit).all();
    }
    После получения списка компаний мы будем перебирать их, получать список контактов и доставать их телефонные номера.

    Код:
    
    const EMPTY_UID '00000000-0000-0000-0000-000000000000';
    /**
    * Получить список контактов
    * @returns Список телефонных номеров
    */
    async function getContactPhones() {
        const 
    companies await getCompanies();
        const 
    phonesTPhone<PhoneType>[] = [];
        for (
    let company of companies) {
            const 
    contactRef company.data._contacts?.find(item => !!item.id);
            if (
    contactRef && contactRef.id != EMPTY_UID) {
                const 
    contact await contactRef.fetch();
                if (
    contact && contact.data._phone && contact.data._phone.length 0) {
                    
    phones.push(...contact.data._phone);
                }
            }
        }
        
    Context.data.phones phones;
    }
    Этот способ позволяет нам получить список телефонных номеров, однако операция выполняется очень долго. Причина в том, что в цикле мы отправляем много запросов. Каждый отдельный запрос выполняется быстро, но когда происходит серия таких запросов, и все они выполняются последовательно, система выдаёт результат только через несколько десятков секунд.

    Чтобы ускорить получение контактов, попробуем выполнить все запросы параллельно:
    Код:
    
    async function getContactPhones() {
        const 
    companies await getCompanies();
        const 
    phonesTPhone<PhoneType>[] = [];
        
    await Promise.all(companies.map(async (company) => {
            const 
    contactRef company.data._contacts?.find(item => !!item.id);
            if (
    contactRef && contactRef.id != EMPTY_UID) {
                const 
    contact await contactRef.fetch();
                if (
    contact && contact.data._phone && contact.data._phone.length 0) {
                    
    phones!.push(...contact.data._phone);
                }
            }
        }));
        
    Context.data.fetchalldebug += `count=${phones.length}`;
        
    Context.data.phones phones;
    }
    В этом примере запросы отправляются одновременно, и в целом мы получаем результат быстрее. Но при большом количестве параллельных запросов могут происходить непредсказуемые зависания. Поэтому данный метод подойдет только для поиска по небольшим объёмам данных. К тому же, на сервер по-прежнему отправляется много запросов, что может негативно сказаться на его производительности.

    Попробуем еще раз оптимизировать код — перенесём функцию в серверный сценарий и вызовем её из клиентского:
    Код:
    
    async function getContactPhones() {
      
    await Server.rpc.getContactPhones();
    }
    Этот способ также позволяет получить список компаний. По времени это занимает чуть дольше, чем предыдущий вариант, зато мы отправляем на сервер всего один запрос. Конечно, скорость запроса будет зависеть от загрузки сервера, но в целом мы получили способ с неплохой производительностью. К тому же, такой подход позволит искать компании и контакты с повышенными привилегиями.

    Если для задачи важна скорость и нет проблем с правами, можно ещё сильнее оптимизировать запрос. Вместо перебора компаний и получения контактов в цикле сразу получим все контакты через поиск:
    Код:
    
    async function getContactPhones() {
        const 
    companies await getCompanies();
        const 
    contacts await Context.fields.contact.app.search()
            .
    where(=> x.__deletedAt.eq(null))
            .
    where(=> x._companies.link(companies)).size(10000)
            .
    all();
        const 
    phonesTPhone<PhoneType>[] = [];
        for (
    let company of companies) {
            const 
    contactRef company.data._contacts?.find(item => !!item.id);
            if (
    contactRef && contactRef.id != EMPTY_UID) {
                const 
    contact contacts.find(item => item.id === contactRef.id);
                if (
    contact && contact.data._phone && contact.data._phone.length 0) {
                    
    phones!.push(...contact.data._phone);
                }
            }
        }
        
    Context.data.phones phones;
    }
    В этом варианте выполняется всего два запроса — получение списка компаний и получение списка контактов. Это происходит довольно быстро, и мы не нагружаем сервер большой очередью запросов. С более детальным сравнением методов можно ознакомиться в статье «Как достать много данных?» в ELMA365 Community.
    Последнее редактирование: 16 авг 2023
  2. fedorova

    fedorova Участник

    Для чего нужна (в каких случаях может быть полезна) проверка на наличие id контакта, и что он не нулевой?
  3. ava_var

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

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

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