Посмотрите, что делает Hibernate
Hibernate: select a1_0.user_id,a1_1.id,a1_1.role from user_role a1_0 join roles a1_1 on a1_1.id=a1_0.role_id where a1_0.user_id=?
Hibernate: select a1_0.user_id,a1_1.id,a1_1.role from user_role a1_0 join roles a1_1 on a1_1.id=a1_0.role_id where a1_0.user_id=?
Hibernate: select a1_0.user_id,a1_1.id,a1_1.role from user_role a1_0 join roles a1_1 on a1_1.id=a1_0.role_id where a1_0.user_id=?
Hibernate: select a1_0.user_id,a1_1.id,a1_1.role from user_role a1_0 join roles a1_1 on a1_1.id=a1_0.role_id where a1_0.user_id=?
Hibernate: select a1_0.user_id,a1_1.id,a1_1.role from user_role a1_0 join roles a1_1 on a1_1.id=a1_0.role_id where a1_0.user_id=?
Он делает отдельный запрос для каждого пользователя, чтобы получить его роли. Я хотел бы избежать этого, поскольку это похоже на убийцу производительности. Я считаю, что есть способ заставить Hibernate выполнять только один запрос для всех пользователей, чтобы получить свои роли за один раз.
Насколько я понимаю, проблема связана с тем, как настроена моя Spring Security.
@Configuration
@Import(WebMvcConfig.class)
@EnableWebSecurity
public class WebSecurityConfig {
private final DataSource dataSource;
private static final String USERS_BY_USERNAME_QUERY = """
SELECT username, password, enabled
FROM users where username = ?
""";
private static final String AUTHORITIES_BY_USERNAME_QUERY = """
SELECT users.username, roles.role
FROM user_role
JOIN users ON user_role.user_id = users.id
JOIN roles ON user_role.role_id = roles.id
WHERE users.username = ?
""";
public WebSecurityConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.requestMatchers("/user/**").hasAuthority("USER")
.requestMatchers("/admin/**").hasAuthority("ADMIN")
.anyRequest().authenticated()
.and().formLogin()
.successHandler(successHandler())
.and().logout().logoutUrl("/logout").logoutSuccessUrl("/login");
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager(dataSource);
userDetailsManager.setUsersByUsernameQuery(USERS_BY_USERNAME_QUERY);
userDetailsManager.setAuthoritiesByUsernameQuery(AUTHORITIES_BY_USERNAME_QUERY);
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsManager);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
@Bean
public AuthenticationSuccessHandler successHandler() {
return (httpServletRequest, httpServletResponse, authentication) ->
httpServletResponse.sendRedirect("/");
}
}
Что мне делать, чтобы избежать чрезмерных SELECT для ролей при работе с Spring Security?
Мои сущности:
@Entity
@Table(name = "users")
@Getter
@Setter
@EqualsAndHashCode
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String name;
@Column(name = "last_name", nullable = false)
private String lastName;
@Column(nullable = false)
private String department;
@Column
private int salary;
@Column(nullable = false)
private byte age;
@Column
private String email;
@Column(name = "enabled", nullable = false)
private byte enabledByte;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_role",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
@EqualsAndHashCode.Exclude
private Set<Role> authorities;
@Entity
@Table(name = "roles")
@Getter
@Setter
@EqualsAndHashCode
public class Role implements GrantedAuthority {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "role", nullable = false, unique = true)
private String authority;
@ManyToMany(mappedBy = "authorities")
@EqualsAndHashCode.Exclude
private Set<User> userList;
Если хотите, можете использовать мои файлы schema.sql и data.sql для быстрой настройки. Он создаст таблицы и заполнит их образцами строк. Я думаю, что было бы лучше не размещать скрипты прямо здесь, чтобы избежать ненужного беспорядка.
Что я пробовал:
EAGER
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_role",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
@EqualsAndHashCode.Exclude
private Set<Role> authorities;
SELECT
все еще присутствовали
@Query
в моем репозитории и передача собственного запроса, который также возвращает ролиpublic interface UserDao extends JpaRepository<User, Long> {
@Query(value = """
SELECT users.id, users.username, users.password, users.name,
users.last_name, users.department, users.salary, users.age,
users.email, users.enabled, roles.role
FROM user_role
JOIN users ON user_role.user_id = users.id
JOIN roles ON user_role.role_id = roles.id
WHERE username != :username
""", nativeQuery = true)
List<User> findAllExceptLoggedUser(String username);
Результат:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: pp.spring_bootstrap.models.User.authorities: could not initialize proxy - no Session
Хочу подчеркнуть, что @ManyToMany(fetch = FetchType.EAGER)
все еще учился в моем User
классе. Если я изменю его на LAZY
, тот же эффект
Обратите внимание, что вопрос выделен жирным шрифтом. Я упоминаю LazyInitializatonException
только для того, чтобы рассказать вам, к чему привела одна из моих попыток решить проблему. Я никогда не спрашивал, как мне избежать этого исключения (по крайней мере, здесь). Вот почему я считаю, что это не делает его расфокусированным
JOIN FETCH
es JSQL помогает в такой ситуации
@Configuration
@EnableWebSecurityConfig
public class WebSecurityConfig {
// ...
@Bean
public UserDetailsService userDetailsService() {
return username -> {
User user = userDao.findByUsername(username);
if (user == null) {
String msg = String.format("No user with username %s is found in the database", username);
throw new UsernameNotFoundException(msg);
}
return user;
};
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
public interface UserDao extends JpaRepository<User, Long> {
@Query(value = """
SELECT u
FROM User u LEFT JOIN FETCH u.authorities
WHERE u.username != :username
""")
List<User> findAllExceptLoggedUser(String username);
@Query(value = """
SELECT u
FROM User u LEFT JOIN FETCH u.authorities
WHERE u.username = :username
""")
User findByUsername(String username);
В консоли вы можете увидеть что-то вроде
Hibernate: select u1_0.id,u1_0.age,a1_0.user_id,a1_1.id,a1_1.role,u1_0.department,u1_0.email,u1_0.enabled,u1_0.last_name,u1_0.name,u1_0.password,u1_0.salary,u1_0.username from users u1_0 left join (user_role a1_0 join roles a1_1 on a1_1.id=a1_0.role_id) on u1_0.id=a1_0.user_id where u1_0.username=?
вы можете использовать JdbcTemplate, который вообще не использует Hibernate/Jpa. Тем не менее, попытка получить все записи имеет смысл только для очень маленьких таблиц.