Питання 25 · Розділ 16

Як уникнути нескінченної рекурсії при серіалізації Entity

При серіалізації bidirectional зв'язків (наприклад, в JSON для REST API) виникає нескінченна рекурсія, що веде до StackOverflowError. Існує кілька підходів до вирішення цієї про...

Мовні версії: English Russian Ukrainian

Огляд

При серіалізації bidirectional зв’язків (наприклад, в JSON для REST API) виникає нескінченна рекурсія, що веде до StackOverflowError. Існує кілька підходів до вирішення цієї проблеми.


🟢 Junior Level

Проблема

При серіалізації bidirectional зв’язків виникає нескінченна рекурсія:

@Entity
public class Order {
    @ManyToOne
    private User user;
}

@Entity
public class User {
    @OneToMany(mappedBy = "user")
    private List<Order> orders;
}

// JSON серіалізація:
// Order → User → Orders → User → Orders → ... → StackOverflowError

Рішення

  1. @JsonIgnore — ігнорувати поле
  2. @JsonManagedReference / @JsonBackReference — розірвати цикл
  3. DTO projection — використовувати DTO замість entity

Це Jackson-специфічні анотації (не JPA!). Працюють тільки при серіалізації через Jackson в Spring Boot REST-контролерах.

// Рішення 1: @JsonIgnore
@Entity
public class User {
    @OneToMany(mappedBy = "user")
    @JsonIgnore  // не серіалізувати
    private List<Order> orders;
}

// Рішення 2: Managed/BackReference
@Entity
public class Order {
    @ManyToOne
    @JsonBackReference  // не серіалізувати
    private User user;
}

@Entity
public class User {
    @OneToMany(mappedBy = "user")
    @JsonManagedReference  // серіалізувати
    private List<Order> orders;
}

🟡 Middle Level

DTO (найкраще рішення)

// DTO — повний контроль над серіалізацією
public record OrderDto(
    Long id,
    String status,
    String userName,
    LocalDateTime createdAt
) {}

// Маппінг
@Query("""
    SELECT new com.example.OrderDto(
        o.id, o.status, u.name, o.createdAt
    )
    FROM Order o JOIN o.user u
    WHERE o.id = :id
    """)
OrderDto findOrderDto(@Param("id") Long id);

Типові помилки

// ❌ Серіалізація entity напряму
@RestController
public class OrderController {
    @GetMapping("/orders/{id}")
    public Order getOrder(@PathVariable Long id) {
        return service.getOrder(id);  // ❌ StackOverflowError
    }
}

// ✅ DTO замість entity
@GetMapping("/orders/{id}")
public OrderDto getOrder(@PathVariable Long id) {
    return service.getOrderDto(id);  // ✅
}

🔴 Senior Level

@JsonIdentityInfo

@Entity
@JsonIdentityInfo(
    generator = ObjectIdGenerators.PropertyGenerator.class,
    property = "id"
)
public class User {
    @OneToMany(mappedBy = "user")
    private List<Order> orders;
}

// Механізм: перша поява об'єкта серіалізується повністю (з "@id": 1).
// Подальші посилання — тільки ID: {"@id": 1}. Це запобігає нескінченному циклу.

Архітектурний підхід

Правило: Ніколи не серіалізуйте JPA entities напряму.

Чому:
1. Нескінченна рекурсія
2. Завантаження lazy полів поза транзакцією
3. Розкриття внутрішньої структури БД
4. Неможливість контролю контракту API

**LazyInitializationException при серіалізації:** коли REST-контролер повертає entity,
серіалізатор Jackson намагається звернутися до LAZY-поля поза транзакцією → exception.

Best Practices

✅ DTO projection (найкраще рішення)
✅ @JsonManagedReference/BackReference (швидке рішення)
✅ @JsonIgnore для однієї сторони
✅ @JsonIdentityInfo для циклічних посилань
✅ Ніколи не серіалізувати entities напряму

❌ Без анотацій (StackOverflowError)
❌ Прямий повернення entity з REST controller

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • Bidirectional зв’язки → нескінченна рекурсія при JSON серіалізації → StackOverflowError
  • 4 рішення: @JsonIgnore, @JsonManagedReference/@JsonBackReference, @JsonIdentityInfo, DTO projection
  • DTO projection — найкраще рішення: повний контроль над API, немає lazy проблем
  • Ніколи не повертати entities з REST controller напряму
  • LazyInitializationException при серіалізації — Jackson звертається до LAZY поза транзакцією

Пов’язані теми:

  • [[24. У чому особливості bidirectional relationships]]
  • [[29. Що таке projection в JPA]]
  • [[4. Що таке LazyInitializationException і як її уникнути]]
  • [[23. Як правильно використовувати @OneToMany і @ManyToOne]]