Проект - oyboy/Jora GitHub Wiki

Проект

Контроллер

Для работы с сущностью предусмотрен один контроллер - ProjectController, в котором определены два get- post-метода. Один служит для вывода списка проектов (см. п. Пользователь-Проект-Роль), newProject и createProject нужны для создания проекта, joinToProject - для присоединения к проекту.

Сервис

Основная работа двух post-методов происходит через ProjectService.

public void saveProject(Project project, User user){
        //Избежание ситуации, когда проект с данным хешем уже существует
        while (projectRepository.findIdByHash(project.getHash()) != null) project.setHash(project.generateHash());

        //Связывание проекта с пользователями
        log.info("Попытка связать {} \n\t с {}", project, user);
        UserProjectRole userProjectRole = new UserProjectRole();
        userProjectRole.setProject(project);
        userProjectRole.setUser(user);
        userProjectRole.setRole(Role.ROLE_LEADER);

        project.getUserProjectRoles().add(userProjectRole);
        user.getUserProjectRoles().add(userProjectRole);

        //Сохранение
        projectRepository.save(project);
        log.info("Saving project: {}", project);
        userProjectRoleReposirory.save(userProjectRole);
    }

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

public void addUserToProject(String project_hash, User user) throws CustomException.UserAlreadyJoinedException,
            CustomException.UserBannedException{
        Project project = projectRepository.findProjectByHash(project_hash);

        // Проверяем существует ли уже связь пользователя с проектом
        if (userProjectRoleReposirory.existsByUserIdAndProjectId(user.getId(), project.getId())) {
            if (userProjectRoleReposirory.isUserBanned(user.getId(), project.getId())) {
                log.warn("User {} is banned in project {}", user.getId(), project.getId());
                throw new CustomException.UserBannedException("Пользователь забанен в этом проекте");
            }
            log.warn("User {} is already a member of project {}", user.getId(), project.getId());
            throw new CustomException.UserAlreadyJoinedException("Пользователь уже добавлен к проекту"); // Или выбрасываем исключение, если нужно
        }

        UserProjectRole userProjectRole = new UserProjectRole();
        userProjectRole.setProject(project);
        userProjectRole.setUser(user);
        userProjectRole.setRole(Role.ROLE_PARTICIPANT);

        log.info("Saving relation: {}", userProjectRole);
        userProjectRoleReposirory.save(userProjectRole);
    }

Здесь уже в контроллере нет аннотации @Valid, поэтому потенциальные проблемы придётся выявлять обращением к бд с помощью двух логических запросов и созданием собственных исключений (см. CustomException).

И последний метод - вывод доступных пользователю проектов:

public List<Project> getProjectsForUser(User user){
        return userProjectRoleReposirory.findProjectsByUserId(user.getId());
    }

Репозиторий

Два запроса, проверяющих существование пользователя и не забанен ли он:

   @Query("SELECT COUNT(*) > 0 " +
           "FROM UserProjectRole upr " +
           "WHERE upr.user.id = :userId AND upr.project.id = :projectId")
   boolean existsByUserIdAndProjectId(@Param("userId") Long userId, @Param("projectId") Long projectId);

   @Query("SELECT upr.banned " +
           "FROM UserProjectRole upr " +
           "WHERE upr.user.id = :userId AND upr.project.id = :projectId")
   boolean isUserBanned(@Param("userId") Long userId, @Param("projectId") Long projectId);

Шаблоны

В home реализована обычная итерация по списку проектов, переданных в Model.attribute, и вывод их полей. В create-project создано два поля под заголовок и описание. ВАЖНО! Для проверки наличия ошибок нужно использовать getFieldError, иначе будет непонятное исключение (якобы переданный объект - null):

<#if errors?? && errors.getFieldError("title")??>
    <div class="error">${errors.getFieldError("title").defaultMessage}</div>
</#if>

Пользователь-Проект-Роль

Эта одна сущность, объединяющая в себе все перечисленные. Она нужна для создания n:n связи между пользователями и проектами и более упрощённого взаимодействия: достаточно обратиться всего к одной таблице, вместо того, чтобы объединять те, что привязаны только к одной сущности.

@Entity
@Data
public class UserProjectRole {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "project_id")
    private Project project;
    @Enumerated(EnumType.STRING)
    private Role role;
    private boolean banned;
}

Позднее было добавлено новое поле banned, которое используется для бана пользователя в некотором проекте. Зачем? Мне кажется это более упрощённая процедура взаимодействия с "нехорошими" пользователями, чем ручное подтверждение каждой заявки на вступление.

Вывод списка проектов для каждого пользователя

Тут участвуют HomeController и ProjectService. Контроллер

    @ModelAttribute(name = "projects")
    public List<Project> getProjects(){
        User user = getUser();
        return projectService.getProjectsForUser(user);
    }
    @GetMapping
    public String home(){return "home";}

Сервис

public List<Project> getProjectsForUser(User user){
        return userProjectRoleReposirory.findProjectsByUserId(user.getId());
    }

Поиск проектов осуществляется новым репозиторием по id пользователя. В список попадают те, в которых пользователь не забанен:

@Query("SELECT p " +
           "FROM Project p " +
           "JOIN UserProjectRole upr " +
           "ON p.id = upr.project.id " +
           "WHERE upr.user.id = :userId AND upr.banned = false")
   List<Project> findProjectsByUserId(@Param("userId") Long userId);
⚠️ **GitHub.com Fallback** ⚠️