Ленивая инициализация @ManyToMany. В чем ошибка? - PullRequest
4 голосов
/ 08 мая 2020

Пожалуйста, помогите мне реализовать ленивую инициализацию.

Я написал код Java в Spring Core, Spring Security и Hibernate. Есть две сущности пользователя и роли. Связываю их с @ManyToMany(fetch = FetchType.EAGER). Но для задачи мне нужно реализовать ленивую инициализацию.

Если я выставил @ManyToMany(fetch = FetchType.LAZY), после успешной авторизации выйдет ошибка:

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: web.model.User.roles, could not initialize proxy - no Session

Google направил по этим ссылкам: Первая ссылка / Вторая ссылка

Предлагается добавить user.getAuthorities () method.size();.

Разве это не обходной путь ???

Тогда авторизация прошла успешно, но при ленивой инициализации я не могу получить доступ к ролям. И они не отображаются на странице jsp.

org.apache.jasper.JasperException: An error occurred during processing [/WEB-INF/pages/admin_panel.jsp] in line [71]

68:             <td>${user.lastName}</td>
69:             <td>${user.username}</td>
70:             <td>${user.password}</td>
71:             <td>${user.roles}</td>
72:             <td>


Stacktrace:
    org.apache.jasper.servlet.JspServletWrapper.handleJspException(JspServletWrapper.java:617)

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

Мой код:

Пользователь

@Entity
@Table(name = "users")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "firstName")
private String firstName;

@Column(name = "lastName")
private String lastName;

@Column(name = "username")
private String username;

@Column(name = "password")
private String password;

@ManyToMany(fetch = FetchType.LAZY) // there was an EAGER here.
@JoinTable(name="user_role",
        joinColumns={@JoinColumn(name="userId")},
        inverseJoinColumns={@JoinColumn(name="roleId")})
private Set<Role> roles;

public User() {
}
...

класс UserServiceImp

@Service
public class UserServiceImp implements UserService {

    @Autowired
    private UserDao userDao;

    @Transactional
    @Override
    public void add(User user) {
        userDao.add(user);
    }

    @Transactional
    public List<User> listUsers() {
        return userDao.listUsers();
    }

    @Transactional
    public boolean deleteUserById(long id) {
        return userDao.deleteUserById(id);
    }

    @Transactional(readOnly = true)
    public User getUserById(long id) {
        return userDao.getUserById(id);
    }

    @Transactional
    public void update(User user) {
        userDao.update(user);
    }

    @Transactional
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userDao.getUserByName(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found");
        }
        int size = user.getAuthorities().size(); //added this line as indicated in the links
        return user;
    }
}

стр. *. jsp

...
<c:forEach items="${users}" var="user">
    <tr>
        <td>${user.id}</td>
        <td>${user.firstName}</td>
        <td>${user.lastName}</td>
        <td>${user.username}</td>
        <td>${user.password}</td>
        <td>${user.roles}</td>
        <td>
            <a href="<c:url value='${pageContext.request.contextPath}/admin/editUser?id=${user.id}'/>" >Edit</a>
            |
            <a href="<c:url value='${pageContext.request.contextPath}/admin/deleteUser/${user.id}'/>" >Delete</a>
        </td>
    </tr>
</c:forEach>
...

UserDaoImp ​​

@Repository
public class UserDaoImp implements UserDao {

    @PersistenceContext
    private EntityManager manager;

   ...

    @Override
    @SuppressWarnings("unchecked")
    public User getUserByName(String username) {
        Query query = manager.createQuery("FROM User u WHERE u.username = : username");
        query.setParameter("username", username);
        try {
            return (User) query.getSingleResult();
        } catch (NoResultException e) {
            return null;
        }
    }
...

1 Ответ

1 голос
/ 08 мая 2020

Что делает FetchType.LAZY, так это то, что он не принимает коллекцию сущностей (или сущность в некоторых случаях, например, @OneToOne), к которой она применяется, когда сущность, содержащая ее, возвращается из базы данных. Он делает дополнительный запрос fetch , а затем , если его метод получения вызывается в той же транзакции . Обходной путь, который вы нашли, работает, потому что вы вызываете геттер getRoles() внутри getAuthorities() (хотя вы не добавили это в вопрос), поэтому явно извлекаете набор ролей.

После этого вы пытаетесь получить доступ к списку пользователей, который, я полагаю, вы получаете от public List<User> listUsers(), и если вы поняли предыдущий абзац, вы, вероятно, уже знаете, что я собираюсь сказать. В этом методе вы не выбираете роли, выходите из транзакции, а затем пытаетесь получить доступ к ролям с помощью <td>${user.roles}</td>. Теперь есть несколько возможных решений этой проблемы.

  • Во-первых, отправка сущностей во внешний интерфейс обычно не лучшая идея, потому что таким образом вы вводите ненужное связывание. Я бы посоветовал вам создать класс UserDto, содержащий все поля, которые вам нужно отобразить (но ни одно больше), который имеет набор RoleDto, к которому применяются те же вещи, и вы конвертируете свой список пользователей, которых вы получаете из базы данных, в список пользователей dtos в методе listUsers(). Таким образом вы решаете 3 проблемы -> 1. ваш код развязан 2. вы устраняете избыточность данных (например, ненужную для полей базы данных внешнего интерфейса) 3. вы выбираете роли каждого пользователя при преобразовании из пользователей в пользовательские dtos.
  • Другой вариант - отделить сущность User от класса, реализующего UserDetails, добавив новый класс для его реализации. Затем вы можете добавить к этому классу при входе в систему всю информацию, необходимую Spring, что в основном означает передачу необходимой информации из класса User в этот новый класс UserDetailsImpl в методе loadUserByUsername(String username), чтобы все методы, унаследованные от UserDetails интерфейс может быть адекватно реализован. С этого момента любая проверка ролей или полномочий будет проходить через не-сущность UserDetailsImpl, что устранит необходимость в вашем обходном пути, и класс никогда не выдаст LazyInitializationException, потому что это не сущность. Но копирование таких полей, как firstName и lastName из User, крайне не рекомендуется из-за возможного несоответствия данных.
  • (НЕ рекомендуется). Если вы чувствуете себя очень ленивым, вы всегда можете просто сделать то же самое обходное решение. в методе listUsers() и просто перебирайте пользователей и вызывайте их метод getRoles() для получения ролей.
  • И, наконец, я понимаю, что по какой-то причине вы не можете этого сделать, и поэтому я оставил это напоследок, но если вам во многих случаях приходится обращаться к коллекции сущностей вне транзакции, то FetchType.EAGER, несомненно, является самым простым и лучшим вариантом. Это буквально проблема, которую он призван решить.
...