В этой статье рассмотрим, какие запросы вызывают исключения NonUniqueResultException и NoResultException, и как их избежать.
NonUniqueResultException выбрасывается, когда ожидается единственная запись, а приходит несколько.
NoResultException выбрасывается, когда ожидается хоть что-то (возвращаемый тип не Optional), а не приходит ничего.
Для устранения исключений обычно нужно сменить тип возвращаемого значения и/или поправить метод. Рассмотрим пример.
Модель
@Data
@NoArgsConstructor
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String name;
//setter/getter/constructor
}
Заполним таблицу данными с повторяющимся дважды значением John:
insert into user (name) values ('Ivan');
insert into user (name) values ('John');
insert into user (name) values ('Petr');
insert into user (name) values ('John');
insert into user (name) values ('Artem');
Задача
Мы ищем пользователей по имени.
Плохой запрос
Такой пользовательский запрос может спровоцировать оба исключения:
public class CustomUserRepositoryImpl implements CustomUserRepository {
@PersistenceContext
private EntityManager em;
@Override
public User findUseByItsName(String name) {
return em.createQuery("select u from User u where u.name =:name", User.class)
.setParameter("name", name)
.getSingleResult();
}
}
Если задать имя John, будет найдено два элемента и возникнет исключение:
org.springframework.dao.IncorrectResultSizeDataAccessException: query did not return a unique result: 2; nested exception is javax.persistence.NonUniqueResultException: query did not return a unique result: 2
Это логично, так как John встречается дважды, а мы берем в запросе getSingleResult().
А если искать по несуществующему значению, например J, то снова проблема:
org.springframework.dao.EmptyResultDataAccessException: No entity found for query; nested exception is javax.persistence.NoResultException: No entity found for query
что в общем тоже логично.
Хороший запрос
- Лучше сменить возвращаемый тип на Optional.
- Но это одно не поможет избавиться от исключений, так что запрос тоже надо изменить.
Можно сделать так (проще):
@Override
public Optional<User> findUserByHisName(String name) {
return em.createQuery("select u from User u where u.name =:name", User.class)
.setParameter("name", name)
.getResultStream().findAny();
}
Либо так (взять Session из EntityManager):
@Override
public Optional<User> findUserByHisName(String name) {
return em.unwrap(Session.class).createQuery("select u from User u where u.name =:name", User.class)
.setParameter("name", name)
.setFirstResult(0)
.setMaxResults(1)
.uniqueResultOptional();
}
Оба варианта избавят нас от обоих исключений.
Запросы по имени метода
Если же говорить о генерируемых из имени метода запросах, то первые два вызывают проблему:
public interface UserRepository extends JpaRepository<User, Long>, CustomUserRepository {
// Optional<User> findByName(String name); //1
// User findByName(String name); //2
Optional<User> findFirstByName(String name); //3
}
Как (1), так и (2) если искать по John спровоцирует
org.springframework.dao.IncorrectResultSizeDataAccessException: query did not return a unique result: 2; nested exception is javax.persistence.NonUniqueResultException: query did not return a unique result: 2
А (3) запрос с ключевым словом First работает идеально.
NoResultException не выбрасывается ни в каком случае, вместо этого возвращается пустой Optional в (1) и null в (2).
Исходный код
Исходный код есть на GitHub.