Hibernate Relationships

Hibernate Many-to-One Mapping Using Annotations

Map many Employee entities to one shared Address using Jakarta Persistence annotations and a foreign-key column.

Many-to-One Association

A many-to-one relationship means that many employees can refer to the same address, while each employee has one address reference.

employee*────────1address

The foreign key is stored in the employee table. The Employee entity is the owning side because it declares @JoinColumn and controls the relationship.

1. Add Maven Dependencies

This example uses Hibernate 6, Jakarta Persistence, and H2.

<dependency>
  <groupId>org.hibernate.orm</groupId>
  <artifactId>hibernate-core</artifactId>
  <version>6.6.9.Final</version>
</dependency>
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <version>2.3.232</version>
  <scope>runtime</scope>
</dependency>

Use the compatible JDBC driver and dialect for your database. Hibernate 5 applications may use javax.persistence, but Jakarta and javax imports must not be mixed.

2. Create the Address Entity

Address is the referenced entity. It does not need a back-reference unless the application must navigate from an address to all employees.

package com.example;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String addressLine;
    private String city;
    private String state;
    private String country;
    private String postalCode;

    protected Address() { }

    public Address(
            String addressLine,
            String city,
            String state,
            String country,
            String postalCode) {
        this.addressLine = addressLine;
        this.city = city;
        this.state = state;
        this.country = country;
        this.postalCode = postalCode;
    }

    public Long getId() { return id; }
    public String getAddressLine() { return addressLine; }
    public String getCity() { return city; }
    public String getState() { return state; }
    public String getCountry() { return country; }
    public String getPostalCode() { return postalCode; }
}

3. Create the Employee Entity

@ManyToOne maps the reference, while @JoinColumn creates the address_id foreign key in the employee table. The relationship is lazy so loading an employee does not automatically load the address.

package com.example;

import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;

@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String email;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "address_id", nullable = false)
    private Address address;

    protected Employee() { }

    public Employee(String name, String email, Address address) {
        this.name = name;
        this.email = email;
        this.address = address;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }
    public Address getAddress() { return address; }
}
Cascade note: the many side should not blindly use CascadeType.ALL when an address can be shared by multiple employees. Persist the address first or use only the cascade operations that match its lifecycle.

4. Configure Hibernate

Place hibernate.cfg.xml in src/main/resources and register both entities.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
  "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
  "https://hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <property name="hibernate.connection.driver_class">org.h2.Driver</property>
    <property name="hibernate.connection.url">jdbc:h2:mem:employee-address;DB_CLOSE_DELAY=-1</property>
    <property name="hibernate.connection.username">sa</property>
    <property name="hibernate.connection.password"></property>
    <property name="hibernate.dialect">org.hibernate.dialect.H2Dialect</property>
    <property name="hibernate.hbm2ddl.auto">create-drop</property>
    <property name="hibernate.show_sql">true</property>
    <mapping class="com.example.Address"/>
    <mapping class="com.example.Employee"/>
  </session-factory>
</hibernate-configuration>

Use Flyway, Liquibase, or another migration tool instead of create-drop for production schema changes.

5. Store Two Employees with One Address

Persist one Address and use it for two Employee objects. This produces one address row and two employee rows that reference it.

try (SessionFactory factory = new Configuration()
        .configure()
        .buildSessionFactory();
     Session session = factory.openSession()) {

    Transaction transaction = session.beginTransaction();
    try {
        Address office = new Address(
            "13 Sector 3",
            "Noida",
            "Uttar Pradesh",
            "India",
            "201301");
        session.persist(office);

        Employee first = new Employee(
            "Ravi Malik", "ravi@example.com", office);
        Employee second = new Employee(
            "Anuj Verma", "anuj@example.com", office);
        session.persist(first);
        session.persist(second);

        transaction.commit();
    } catch (RuntimeException exception) {
        transaction.rollback();
        throw exception;
    }
}

6. Fetch Employees and Their Address

Use a typed HQL fetch join when the result requires both employees and addresses.

List<Employee> employees = session
    .createQuery(
        "select e from Employee e "
        + "join fetch e.address",
        Employee.class)
    .getResultList();

for (Employee employee : employees) {
    Address address = employee.getAddress();
    System.out.println(
        employee.getName() + " - "
        + address.getCity());
}

Access a lazy association while the session is open, or fetch it explicitly as shown. Avoid returning entities with uninitialized relationships from a closed-session web request.

Optional Bidirectional Mapping

If the application must find all employees for an address, add a collection to Address:

@OneToMany(mappedBy = "address")
private List<Employee> employees = new ArrayList<>();

The Employee field remains the owning side. Maintain both sides with helper methods and avoid eager loading a large employee collection.

Many-to-One Best Practices

  • Put the foreign key on the many-side table.
  • Use optional = false and a non-null join column when every employee must have an address.
  • Do not use CascadeType.REMOVE when the referenced address can be shared.
  • Use lazy loading and deliberate fetch joins.
  • Add an index on the foreign-key column for large employee tables.
  • Use database constraints to enforce referential integrity.
  • Exclude associations from equals() and hashCode().
  • Use schema migrations in production.
Result: many employees reference one address through the employee.address_id foreign key.