Найти проблемы в JPA-сущностях и сервисе

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.