| Annotation | Example | DB Representation |
|---|---|---|
| @OneToOne | User ↔ UserProfile | Foreign key in one table |
| @OneToMany | User -> Orders | Foreign key in child table |
| @ManyToOne | Order -> User | Foreign key in this table |
| @ManyToMany | Student ↔ Course | Join table |
// @OneToOne: User has one UserProfile
@Entity
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
// Owning side: has the foreign key column
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id", unique = true)
private UserProfile profile;
}
@Entity
public class UserProfile {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String bio;
private String website;
// Inverse side (mappedBy = field name in User)
@OneToOne(mappedBy = "profile")
private User user;
}
// @OneToMany / @ManyToOne: User has many Orders
@Entity
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
// One user has many orders
// mappedBy = field name in Order that owns the relationship
@OneToMany(mappedBy = "user",
cascade = CascadeType.ALL,
fetch = FetchType.LAZY,
orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
// Helper method to maintain bidirectional consistency
public void addOrder(Order order) {
orders.add(order);
order.setUser(this);
}
}
@Entity
@Table(name = "orders")
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private java.math.BigDecimal total;
// Many orders belong to one user
// This side owns the relationship (has the FK column)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
}
| Cascade Type | Description |
|---|---|
| ALL | All operations cascade (PERSIST, MERGE, REMOVE, REFRESH, DETACH) |
| PERSIST | Save cascades to related entities |
| MERGE | Merge cascades to related entities |
| REMOVE | Delete cascades to related entities |
| REFRESH | Refresh cascades to related entities |
| DETACH | Detach cascades to related entities |
// @ManyToMany: Student can enroll in many Courses
// Course can have many Students
@Entity
public class Student {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Owning side: defines the join table
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE},
fetch = FetchType.LAZY)
@JoinTable(
name = "student_course", // Join table name
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
}
@Entity
public class Course {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
// Inverse side
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
}
// Usage
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
Student student = new Student("Alice");
Course java = new Course("Java Programming");
Course spring = new Course("Spring Framework");
student.getCourses().add(java);
student.getCourses().add(spring);
java.getStudents().add(student);
spring.getStudents().add(student);
session.persist(student);
session.persist(java);
session.persist(spring);
tx.commit();
session.close();
// EAGER: related entities loaded immediately with parent
// Default for @ManyToOne and @OneToOne
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "department_id")
private Department department; // Loaded with Employee in same query
// LAZY: related entities loaded only when accessed
// Default for @OneToMany and @ManyToMany
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Order> orders; // Loaded only when orders is accessed
// LAZY loading requires open session when accessing the collection
// Use JOIN FETCH in HQL to avoid N+1 problem:
List<User> users = session.createQuery(
"SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.orders", User.class)
.list();
// This loads users AND their orders in a single query
An association must identify which side writes the foreign key and how both object references stay synchronized in memory.
Cascades and orphan removal cross row-lifecycle boundaries. Apply them only when the related entity is truly owned by the same aggregate; shared references need independent lifecycle rules.
A bidirectional association is two Java references representing one database relationship. Helper methods should update both sides so the in-memory graph does not disagree with the foreign key owner. equals and hashCode must not traverse mutable relationships or force large lazy graphs to load.
Default associations to the smallest useful fetch plan, then select joins or entity graphs per use case. Test serialization and logging so they do not trigger lazy loads or recursion. For large collections, query a bounded page rather than loading every child through the parent.
The owning side is the mapping without mappedBy, usually the @ManyToOne side for a one-to-many relationship. Changes made only to the inverse collection may not update the foreign key.
A lazy collection is represented by a proxy that needs an open persistence context to load its rows. After the Session closes, the proxy cannot execute the query and may throw LazyInitializationException.
ALL includes REMOVE. In a many-to-many association, the related entity is usually shared, so deleting one parent should remove join-table rows rather than delete the shared child.
Explore 500+ free tutorials across 20+ languages and frameworks.