40. Найти проблемы в JPA-сущностях и сервисе
Условие задачи:
Даны JPA-сущности User и Address, связанные отношением ManyToOne / OneToMany.
Также задан сервис UserService, который получает пользователя и обращается к его адресу.
При этом:
spring.jpa.open-in-view=false
Необходимо найти проблемы в коде и определить, выполнится ли обращение к адресу, если пользователь найден.
Код:
@Entity
@Data
class User {
@Id
private Integer id;
@ManyToOne(
fetch = FetchType.LAZY,
optional = false,
cascade = CascadeType.ALL
)
@JoinColumn(name = "id", nullable = false)
private Address address;
}
@Entity
@Data
class Address {
@Id
private Integer id;
private String description;
@OneToMany(
mappedBy = "address",
fetch = FetchType.LAZY,
cascade = CascadeType.ALL
)
List<User> users;
}
@Service
@RequiredArgsConstructor
@Slf4j
class UserService {
private UserRepository userRepository;
public User findById(Integer id) {
User foundUser =
userRepository.findById(id).orElse(null);
if (foundUser != null) {
log.info(
"Пользователь живет по адресу: {}",
foundUser.getAddress().getDescription()
);
}
return foundUser;
}
}
Спойлеры к решению
Подсказки
@RequiredArgsConstructor создаёт конструктор для final-полей.💡 Проверь, правильно ли указана колонка в
@JoinColumn.💡
address загружается лениво через FetchType.LAZY.💡 При
open-in-view=false для обращения к lazy-связи нужен открытый Persistence Context.💡
@Data может быть проблемным для двунаправленных JPA-связей.Решение
В коде есть несколько проблем.
1. UserRepository не внедрится
Поле:
private UserRepository userRepository;
не является final, поэтому @RequiredArgsConstructor не добавит его в конструктор.
Нужно:
private final UserRepository userRepository;
Иначе вызов:
userRepository.findById(id)
приведёт к NullPointerException.
2. Неверный @JoinColumn
Сейчас:
@JoinColumn(name = "id")
Колонка id уже используется как первичный ключ User.
Для внешнего ключа нужна отдельная колонка:
@JoinColumn(name = "address_id", nullable = false)
3. Проблема с LAZY
Связь:
@ManyToOne(fetch = FetchType.LAZY)
загружается лениво.
При этом:
spring.jpa.open-in-view=false
а метод сервиса не помечен @Transactional.
Поэтому после findById() обращение:
foundUser.getAddress().getDescription()
может привести к:
LazyInitializationException
Исправленный сервис:
@Service
@RequiredArgsConstructor
@Slf4j
class UserService {
private final UserRepository userRepository;
@Transactional(readOnly = true)
public User findById(Integer id) {
User foundUser =
userRepository.findById(id).orElse(null);
if (foundUser != null) {
log.info(
"Пользователь живет по адресу: {}",
foundUser.getAddress().getDescription()
);
}
return foundUser;
}
}
Сущность:
@Entity
@Getter
@Setter
class User {
@Id
private Integer id;
@ManyToOne(
fetch = FetchType.LAZY,
optional = false
)
@JoinColumn(
name = "address_id",
nullable = false
)
private Address address;
}
CascadeType.ALL на ManyToOne обычно не нужен, поскольку удаление пользователя не должно автоматически удалять общий адрес.
Также @Data нежелателен для двунаправленных JPA-связей: сгенерированные toString(), equals() и hashCode() могут затрагивать связанные сущности и приводить к рекурсии.
Таким образом, исходный код корректно не выполнится: сначала возможен NullPointerException из-за userRepository, а после исправления DI и маппинга при обращении к LAZY-адресу без транзакции возможен LazyInitializationException.