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

У чому особливості bidirectional relationships

Bidirectional relationships (двонаправлені зв'язки) дозволяють навігацію між сутностями в обох напрямках. Вони вимагають ручної синхронізації обох сторін та правильного використ...

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

Огляд

Bidirectional relationships (двонаправлені зв’язки) дозволяють навігацію між сутностями в обох напрямках. Вони вимагають ручної синхронізації обох сторін та правильного використання mappedBy.


🟢 Junior Level

Що таке bidirectional relationship

Це зв’язок, де обидві сторони знають одна про одну.

// Order знає про User
@Entity
public class Order {
    @ManyToOne
    private User user;
}

// User знає про Orders
@Entity
public class User {
    @OneToMany(mappedBy = "user")
    private List<Order> orders;
}

Owner side та Inverse side

Owner side — сторона з FK (Order з @ManyToOne).

Inverse side — сторона з mappedBy (User з @OneToMany(mappedBy = "user")).


🟡 Middle Level

Синхронізація обов’язкова

@Entity
public class User {
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Order> orders = new ArrayList<>();

    // Helper methods — обов'язково!
    public void addOrder(Order order) {
        orders.add(order);
        order.setUser(this);  // ОБОВ'ЯЗКОВО!
    }

    public void removeOrder(Order order) {
        orders.remove(order);
        order.setUser(null);  // ОБОВ'ЯЗКОВО!
    }
}

Чому розсинхронізація — проблема

// ❌ Розсинхронізація
User user = new User();
Order order = new Order();
user.getOrders().add(order);

// order.getUser() == null → FK не встановиться!

// ✅ Синхронізація через helper
user.addOrder(order);  // setter + setUser
// order.getUser() == user → FK встановиться

🔴 Senior Level

equals/hashCode для bidirectional

// Використовуйте business key, НЕ ID!
@Entity
public class User {
    @Column(unique = true, nullable = false)
    private String email;  // business key

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return email != null && email.equals(user.email);
    }

    @Override
    public int hashCode() {
        return email != null ? email.hashCode() : 0;
    }
}

Чому не ID для equals/hashCode

Проблеми з ID:
1. Transient entity (id=null) != Transient entity (id=null)
2. HashSet втрачає entity після persist (id змінюється)
3. Неможливо додати в HashSet до persist

**Якщо немає natural/business key:** використовуйте UUID, що призначається при конструюванні об'єкта.

Best Practices

✅ Helper methods для синхронізації
✅ Business key для equals/hashCode
✅ mappedBy на inverse side
✅ Синхронізація обох сторін
✅ Ініціалізація колекцій

❌ ID для equals/hashCode
❌ Без helper methods
❌ Розсинхронізація

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

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

  • Bidirectional = навігація в обидві сторони, вимагає синхронізації обох сторін
  • Owner side — з FK (@ManyToOne), Inverse side — з mappedBy (@OneToMany)
  • Helper methods ОБОВ’ЯЗКОВІ: addOrder/addItem синхронізують обидві сторони
  • equals/hashCode по business key (email, SKU), НЕ по ID
  • mappedBy тільки на inverse side

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

  • [[23. Як правильно використовувати @OneToMany і @ManyToOne]]
  • [[22. Що таке orphan removal]]
  • [[25. Як уникнути нескінченної рекурсії при серіалізації Entity]]
  • [[20. Як працюють каскадні операції (Cascade)]]