Spring + Hibernate: Как эффективно объединить две таблицы связей и включить полученные данные в один объект? (Пользователь-ролевой справа)

Укороченная версия

У меня есть базовая настройка, где пользовательская таблица связана с таблицей ролей, а таблица ролей связана с правом. Это оба отношения "многие ко многим". Роли являются динамической сущностью и не представляют интереса для приложения (только для визуальных аспектов). Когда я выбираю пользователя, я хочу вернуть данные в пользовательскую таблицу, включая список имен всех прав.

Чтобы уточнить, это то, что я хочу сделать решение:

обзор решения

Мне удалось получить права на мой пользовательский объект и вернуть их, но это неэффективно из-за дополнительных вызовов запроса, которые hibernate делает после вызова исходного запроса.

Подробная версия

Позвольте мне сначала дать вам некоторую информацию о том, как связаны сущности и как выглядит код. Моя (упрощенная) структура таблицы базы данных выглядит следующим образом:

Диаграмма базы данных

User.java

@Entity
@Table(name = "user")
public class User {

  @Id
  @Column(name = "user_id", columnDefinition = "user_id")
  private Long userId;

  @Transient
  private List<String> rights

  @ManyToMany
  @JoinTable(
    name = "user_role",
    joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "user_id"),
    inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "role_id"))
  @JsonIgnore
  private List<Role> roles;

  //Getters and setters
}

Role.java

@Entity
@Table(name = "role")
public class Role {

  @Id
  @Column(name = "role_id", columnDefinition = "role_id")
  private Long roleId;

  @ManyToMany
  @JoinTable(
    name = "user_role",
    joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "role_id"),
    inverseJoinColumns = @JoinColumn(name = "user_id", referencedColumnName = "user_id"))
  @JsonIgnore
  private List<Employee> employees;

  @ManyToMany
  @JoinTable(
    name = "role_right",
    joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "role_id"),
    inverseJoinColumns = @JoinColumn(name = "right_id", referencedColumnName = "right_id"))
  @JsonIgnore
  private List<Right> rights;


  //Getters and setters
}

Right.java

@Entity
@Table(name = "right")
public class Right {

  @Id
  @Column(name = "right_id", columnDefinition = "right_id")
  private Long rightId;

  @Column(name = "name", columnDefinition = "name")
  private String name;

  @ManyToMany
  @JoinTable(
    name = "role_right",
    joinColumns = @JoinColumn(name = "right_id", referencedColumnName = "right_id"),
    inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "role_id"))
  @JsonIgnore
  private List<Role> roles;

  //Getters and setters
}

Важно знать, что я использую API спецификаций Java для объединения таблиц:

return (root, query, cb) -> {
  query.distinct(true);
  Join rolesJoin = root.join("roles", JoinType.LEFT);
  Join rightsJoin = rolesJoin.join("rights", JoinType.LEFT);
  return cb.conjunction();
};

Это создает правильный запрос:

select <columns go here> 
from employee user0_
left outer join user_role roles1_ on user0_.user_id=roles1_.user_id
left outer join role role2_ on roles1_.role_id=role2_.role_id
left outer join role_right rights3_ on role2_.role_id=rights3_.role_id
left outer join right right4_ on rights3_.right_id=right4_.right_id

Все выглядело хорошо для меня до сих пор. Но когда я попытался получить имена всех ролей, там выполнялось более двух запросов (считая за страницу и исходный)

//The original code uses lambda for this
for(Role role : user.getRoles()){
  for(Right right: role.getRights()){
    user.addRight(right.getName());
  }
}

Дополнительный запрос выглядит так:

select <column stuff> 
from role_right rights0_
inner join right right1_ on rights0_.right_id=right1_.right_id
where rights0_.role_id=?

Это делает звонок очень неэффективным для меня. В этом случае это один пользователь, но с несколькими пользователями это складывается.

Есть ли способ сделать так, чтобы в один запрос помещались имена всех прав в пользовательской сущности без добавления дополнительных запросов?

Вещи, которые я пробовал до сих пор:

  • С помощью @SecondaryTable непосредственно определить столбец из таблицы Right в моей сущности User. Мне не удалось сначала связать роль с пользователем, а затем использовать поля из таблицы ролей, чтобы связать нужную таблицу. Таким образом, в конце я должен добавить аннотацию @SecondaryTable поверх моего объекта User и определить столбцы объекта Right ниже.

  • С помощью @Formula в сущности User, чтобы вставить собственный запрос в запрос. Это также не сработало, так как аннотация не понимала, как отобразить все в список прав.

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

Заранее спасибо,
Робин

1 ответ

Решение

Ты используешь Root.join который просто объединяет таблицы для целей запроса; Ленивые ассоциации в загруженных объектах по-прежнему не будут инициализированы.

Как я вижу, ваше намерение также состоит в том, чтобы инициализировать ленивые коллекции. Для этого вы должны использовать Root.fetch (определенный в методе интерфейса, унаследованном от FetchParent):

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

Однако ваше намерение не является хорошей практикой; не объединяйте несколько коллекций в одном запросе, иначе набор результатов запроса будет разнесен с полным декартовым произведением между объединенными коллекциями. Ваш набор результатов содержит <num of users> * <num of roles per user> * <num of rights per role> строк. Итак, каждый пользовательский данные повторяется <num of roles per user> * <num of rights per role> раз в сгенерированном наборе результатов.

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

Другие вопросы по тегам