Как побороть UnsupportedOperationException: null в Spring?

128
12 декабря 2019, 17:50

Столкнулся с довольно странной проблемой. Имеется entity User и объекты этого класса нужно связать друг с другом через many-to-many relationship. Соответственно, помимо основной таблицы "user" должны появиться еще 2 таблицы "customer_authors" "author_customers"

@Entity
public class User {
  @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    // ID пользователя
    private Long userId;

    // "customer_authors"
    @ManyToMany(cascade=CascadeType.ALL)
    @JoinTable(name = "customer_authors",
            joinColumns = @JoinColumn(name = "customer_id"),
            inverseJoinColumns = @JoinColumn(name = "author_id"))
    private List<User> authorsList = new ArrayList<>();
    // "author_customers"
    @ManyToMany(cascade=CascadeType.ALL)
    @JoinTable(name = "author_customers",
            joinColumns = @JoinColumn(name = "author_id"),
            inverseJoinColumns = @JoinColumn(name = "customer_id"))
    private List<User> customersList = new ArrayList<>();
    // getters, setters and other data.
}

Нигде в моем коде не используется Arrays asList() / не создается список с фиксированным размером. Именно, это является причиной подобной ошибки согласно поиску в гугл и stackoverflow...

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

user.setUserEmail(userEmail);
user.setUserPassword(passwordEncoder.encode(userPassword));
userRepository.save(user);
User referrer = userRepository.findUserByUserId(referrerId);
referrer.getAuthorsList().add(user);
userRepository.save(referrer);

то получаю соответствующую ошибку -

UnsupportedOperationException
// excerpt
java.lang.UnsupportedOperationException: null
  at com.sun.proxy.$Proxy142.save(Unknown Source) ~[na:na]
  at info.md7.textpool.services.UserService.addUser(UserService.java:252) ~[classes/:na]
  at info.md7.textpool.controllers.UserController.userRegistration(UserController.java:54) ~[classes/:na]

Но при этом, если для теста в контроллере использовать что-то подобное:

User customer = (User) userService.findUserByEmail(currentUser.getUsername());
    User author = userRepository.findByUserEmail("sukkivulmo@desoz.com");
    customer.getAuthorsList().add(author);
    userRepository.save(author);

то действительно, в таблицу добавляется нужная информация ID реферрера и ID пользователя.

В чем может быть заключаться ошибка? Может быть, кто-то сталкивался с этим и знает, как исправить? Заранее благодарю!

Полный сниппет метода из UserService:

public boolean addUser(  //todo добавить тип работы
          String userFullname,
          String userEmail,
          String userPassword,
          String prefLangs,
          String prefCats,
          Double paymentCost,
          String paymentMethod,
          String paymentWallet,
          Long referrerId,
          User user
  ) throws MessagingException {
    User userFromDbEmail = userRepository.findByUserEmail(userEmail);
    User referrer = userRepository.findUserByUserId(referrerId);
    if(userFromDbEmail != null) {
      return false;
    }
    /*
     * Проверяем наличие данных в полях prefLang, prefCats, paymentCost, paymentMethod, paymentWallet.
     * И если они имеются в одном из полей, то регистрируем пользователя, как Автора.
     * В противном случае, создаем нового Заказчика.
     *
     */
    if(
        prefLangs != null && !prefLangs.isEmpty() ||
        prefCats != null && !prefCats.isEmpty() ||
        paymentCost != null ||
        paymentMethod != null && !paymentMethod.isEmpty() ||
        paymentWallet != null && !paymentWallet.isEmpty()
    ) {
      user.setUserFullname(userFullname);
      user.setUserEmail(userEmail);
      user.setUserPassword(passwordEncoder.encode(userPassword));
      user.setRegDate(LocalDateTime.now());
      user.setUserActive(false);
      user.setRoles(Collections.singleton(Role.AUTHOR));
      user.setActivationCode(UUID.randomUUID().toString());
      user.setUserMode("author");
      user.setReceiveEmails(true);
      // Если пользователь был приглашен, то находим реферрера и добавлем в его список нового пользователя
      User referrer = userRepository.findUserByUserId(referrerId);
      user.setReferrerId(referrer);
      userRepository.save(user);

      /*  Добавляем метаданные для юзера
          С связи с особенностями верстки, данные скриптом вставляю в hidden input поле, после чего
          получаю String разделенный запятыми и разобрав добавляю данные в user_meta
       */
      assert prefLangs != null;
      String[] prefLang = prefLangs.split(",");
      for (String metaValue : prefLang) {
        UserMeta userMeta = new UserMeta();
        String metaKey = "prefLang";
        userMeta.setMetaKey(metaKey);
        userMeta.setMetaValue(metaValue);
        userMeta.setUser(user);
        userMetaRepository.save(userMeta);
      }
      assert prefCats != null;
      String[] prefCat = prefCats.split(",");
      for (String metaValue : prefCat) {
        UserMeta userMeta = new UserMeta();
        String metaKey = "prefCat";
        userMeta.setMetaKey(metaKey);
        userMeta.setMetaValue(metaValue);
        userMeta.setUser(user);
        userMetaRepository.save(userMeta);
      }
      UserMeta paymentMeta = new UserMeta();
      paymentMeta.setMetaKey(paymentMethod);
      paymentMeta.setMetaValue(paymentWallet);
      paymentMeta.setUser(user);
      userMetaRepository.save(paymentMeta);
      if(paymentCost != null) {
        UserMeta paymentCostMeta = new UserMeta();
        paymentCostMeta.setMetaKey("paymentCost");
        paymentCostMeta.setMetaValue(paymentCost.toString());
        paymentCostMeta.setUser(user);
        userMetaRepository.save(paymentCostMeta);
      }

      referrer.getAuthorsList().add(user);
      userRepository.save(referrer);
    } else {
      user.setUserFullname(userFullname);
      user.setUserEmail(userEmail);
      user.setUserPassword(passwordEncoder.encode(userPassword));
      user.setRegDate(LocalDateTime.now());
      user.setUserActive(false);
      user.setRoles(Collections.singleton(Role.CUSTOMER));
      user.setActivationCode(UUID.randomUUID().toString());
      user.setUserMode("customer");
      user.setReceiveEmails(true);
      // Если пользователь был приглашен, то находим реферрера и добавлем в его список нового пользователя
      User referrer = userRepository.findUserByUserId(referrerId);
      user.setReferrerId(referrer);
      userRepository.save(user);

      referrer.getCustomersList().add(user);
      userRepository.save(referrer);
    }
    return true;
  }
Answer 1

Откуда взялся UnsupportedOperationException?

Коллекции в Java могут иметь необязательные для реализации методы. При вызове метода, имплементация которого не предусмотрена в данной реализации выбрасывается UnsupportedOperationException:

public interface Iterator<E> {
    //...
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
    //...
} 

Наличие подобных методов в интерфейсе может вызвать недоумение, однако в данном случае, насколько я понимаю, это вызвано поддержкой обратной совместимости с более старыми версиями Java.

Как уже говорилось другими участниками, коллекции могут быть изменяемыми и неизменяемыми. Однако, вопреки общему мнению в данном случае исключение возникает не из-за использования Arrays.asList(автор явно инициализирует поля новым объектом ArrayList'а). Хотя это было очень-очень близко и копать явно нужно было в эту сторону.

К сожалению, всё внимание было приковано к двум полям: authorsList и customersList. Там были очень сомнительные зеркальные связи, да ещё и с каскадными операциями, которые в данном конкретном случае явно не могли быть применены в том виде. Более того при удалении cascade=CascadeType.ALL ошибка пропадала. К ним мы вернемся позже.

Ошибка была куда прозаичнее:

user.setRoles(Collections.singleton(Role.AUTHOR));

Автор явно создаёт синглтон(немодифицируемую коллекцию) и передаёт его в список ролей.

После чего данный объект успешно сохраняется.

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

Сохранив второго пользователя и его связь с первым, hibernate смотрит на cascade=CascadeType.ALL над customersList и начинает обновлять объект первого пользователя, который лежал в данном списке. Перезаписывает все поля, встречающиеся в полях коллекции hibernate чистит после чего извлекает в них данные заново. И так он доходит до поля roles с синглтоном, который лежит в первом пользователе, который в свою очередь лежит внутри списка второго пользователя. Пытается очистить его и получает UnsupportedOperationException.

Чтобы исправить это достаточно просто написать:

  user.getRoles().add(Role.AUTHOR);

И cascade=CascadeType.ALL в данном случае не при чем. Тем не менее его оставлять так нельзя. Потому что, при удалении приглашающего пользователя автоматически будут удалены и все авторы и клиенты, что как мне кажется не соответствует замыслам автора.

Над данными двумя полями(authorsList и customersList) стоит как минимум поставить

@ManyToMany(cascade={CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH}, targetEntity = User.class)

А если говорить честно, то данные поля являются избыточными и от них можно просто избавиться. Ведь для каждого приглашенного пользователя мы устанавливаем referrer(пригласившего пользователя). Соответственно мы можем либо добавить обратное свойство invitedUsers и отфильтровать его по роли. Либо просто выбрать пользователей по referrer'у и роли.

Answer 2

Вам нужно посмотреть в сторону Array list он возвращает list, Arrays.asList возвращает список фиксированного размера, в который нельзя добавлять элементы. Изменяемый список можно создать как-то так

List<Integer> list1 = new ArrayList<>(Arrays.asList(10,20,60,30,22,70,89));
READ ALSO
Почему intellij не может найти аннотацию @Max?

Почему intellij не может найти аннотацию @Max?

Разбираюсь с аннотациямиХочу указать аннотацию @Max

100
Progress Bar цвет пустой части

Progress Bar цвет пустой части

Как можно настроить Progress Bar, чтобы было примерно как на скрине

113
Можно ли передать информацию из одного Intent в две разных активити?

Можно ли передать информацию из одного Intent в две разных активити?

Можно ли из 1 intent предать информацию в 2 разных Activity, по нажатию кнопки// Это о куда надо предать картинку

120
Создание grid сетки с разной высотой

Создание grid сетки с разной высотой

Собственно, что хочу и что получаю:

128