Интеграция Primefaces в приложение на Spring Boot. Часть 6 — Комбинирование компонентов для вывода сложных данных

90b444e755a1de1d341ee36319c4715e.png

В предыдущей части статьи мы уже познакомились с тем, как выводить на страницу данные с помощью отдельных простых компонентов. Как мы убедились, для этих целей можно использовать, как компоненты самого Primefaces, так и компоненты фреймворка JSF, на котором он основан. В частности, h:outputText — пример компонента JSF, а Data List — компонент Primefaces. В моих примерах и в документации их легко различить, например, по префиксу, JSF компоненты имеют префикс h, а Primefaces компоненты — префикс p, что также отражено в декларации xhtml документа, редко кто использует нестандартные префиксы, но если это так, то это отражено в декларации.

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

  • туториал по базовым тегам JSF — на этом же сайте можно найти множество туториалов по более сложным и композитным компонентам JSF.

  • документация по UI Primefaces — здесь вы найдете описание десятков компонентов Primefaces с showcase, примерами их использования с образцами кода управляемых бинов и xhtml страниц.

Однако, как я уже писал ранее, следует иметь ввиду, что применение Primefaces может приводить к неожиданным side эффектам и не исчерпывается сведениями из документации. Например, вам требуется вывести сложно взаимосвязанные данные в каком-либо специфическом виде, и вам не хватает возможностей типовых компонентов из документации JSF и Primefaces. И вот тут уже придется потрудиться самостоятельно, применив смекалку разработчика. Не сомневаюсь, что практически все проблемы можно решить, комбинируя простые (или не очень простые) стандартные компоненты в более сложные, составные конструкции. Фактически, вы можете сделать собственные композитные компоненты на базе стандартных.

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

f0f48e779c83667900a0fa5912d8ab02.png

получаем из таблицы для какого-то конкретного сотрудника уровень владения определенной компетенцией, у меня это — нативный запрос, что связано с конкретной реализацией структуры данных. У вас это может быть совсем другая структура и другой запрос к репозиторию, например, на базе Spring Data JPA, конкретные детали здесь приведены лишь для ясности описания моего конкретного примера:

@Repository
public interface SkillRepository extends JpaRepository {
    Skill findByName(String name);

    @Query(value = "select skill_grade_id from employees_to_skills where ( skill_id = :skill_id and employee_id = :employee_id )", nativeQuery = true)
    Integer getSkillGradeIdByEmployeeIdAndSkillId(Integer skill_id, Integer employee_id);
}

далее в сервисе получаем для данного конкретного сотрудника полную карту его компетенций с соответствующим каждой компетенции уровнем владения

public Map getSkillMap(Employee employee) {
    Map map = new HashMap<>();
    Set skills = employee.getSkills();
    skills.forEach(skill -> {
        map.put(
                skill,
                skillGradeRepository.getReferenceById(skillRepository.getSkillGradeIdByEmployeeIdAndSkillId(skill.getId(), employee.getId()))
        );
    });
    return map;
}

Давайте теперь выведем список всех компетенций сотрудника, указав рядом с каждой компетенцией уровень владения ею данным сотрудником в виде красивого рейтинга со звездочками. Для этого нам придется скомбинировать два компонента — Data List p:dataList и вложненный в него рейтинг p:rating. Для этого добавим на xhtml страницу такой код:

Квалификационный профиль сотрудника

#{entry.key.name}

И создадим класс управляемого бина компонента для вывода данной информации:

import jakarta.inject.Inject;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;
import org.satel.ressatel.entity.Employee;
import org.satel.ressatel.entity.Skill;
import org.satel.ressatel.entity.SkillGrade;
import org.satel.ressatel.service.EmployeeService;
import org.satel.ressatel.service.SkillService;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Component("employeeSkillRatingView")
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
@Getter
@Setter
@Log4j2
public class EmployeeSkillRatingView {
    private String id;
    private List> skillList;
    private final EmployeeService employeeService;
    private final SkillService skillService;

    @Inject
    public EmployeeSkillRatingView(EmployeeService employeeService, SkillService skillService) {
        this.employeeService = employeeService;
        this.skillService = skillService;
    }


    public void onload() {
        Employee employee = employeeService.getByStringId(id);
        if (!skillService.getSkillMap(employee).isEmpty()) {
            skillList = new ArrayList<>(skillService.getSkillMap(employee).entrySet());
        }
    }

}

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

ac0f1b96fa18541957277859a0752994.png

Здесь вы еще видите отдельную кнопку «Редактировать». И редактирование композитных компонентов, составленных из нескольких базовых, также является интересной и не тривиальной задачей — ее мы разберем в продолжениях статьи.

Дополнительным бонусом поясню еще один тонкий момент в коде данного компонента. Обратите внимание, что класс компонента помечен аннотацией

@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)

Это аннотация из Spring, и означает она, что новый экземпляр бина компонента будет создаваться каждый раз при запросе компонента из новой xhtml страницы в нашем приложении. Особенно важно это, когда у вас в приложении есть списки сущностей и карточки отдельных сущностей. Например, если бы этой аннотации не было, то происходило бы следующее: вы открываете карточку сотрудника и видите данные, полученные компонентом для этой карточки. Затем закрываете карточку и открываете карточку второго сотрудника, и вместо данных для него вы увидите в ней данные компонента, полученные для первого же сотрудника. Вы не сможете получить уникальные данные компонента для каждого сотрудника отдельно. Однако, как вы уже догадались, в Jakarta EE есть собственная аннотация @RequestScoped, которая, казалось бы, призвана выполнять ту же самую функцию. Но предупреждаю сразу — в моей конфигурации приложения (то есть при интеграции Srping + JSF + Primefaces) эта аннотация не работает, один и тот же бин все равно создается в первой же карточке сотрудника и затем подставляется в остальные карточки со всеми своими данными, что нам категорически не нужно — то есть аннотация @RequestScoped не является полным аналогом соответствующей аннотации Spring, по крайней мере, при интеграции Spring и Primefaces. Вполне возможно, что в приложении на чистой Jakarta EE эта аннотация и будет работать, но не в нашем случае.

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

© Habrahabr.ru