Hibernate Relationships

Hibernate One-to-One Example Using Annotations

Map one Employee to one Address with a bidirectional Hibernate relationship and a shared primary key.

One-to-One Association

A one-to-one relationship means that one employee has one address and one address belongs to one employee. This page uses a shared-primary-key design: the Address identifier is also a foreign key to Employee.

employee1────────1address

The foreign key is stored in the address table, not the employee table. This is a reliable way to enforce that an address belongs to at most one employee.

1. Add Maven Dependencies

This example uses Hibernate 6, Jakarta Persistence, and H2 for a self-contained database.

<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>

For Hibernate 5, the equivalent persistence imports use javax.persistence. Do not mix the Jakarta and javax namespaces in one application.

2. Create the Employee Entity

The Employee entity is the parent in this example. cascade = CascadeType.ALL allows the address to be persisted with the employee, while orphanRemoval removes an address that is detached from its employee.

package com.example;

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

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

    private String name;
    private String email;

    @OneToOne(
        mappedBy = "employee",
        cascade = CascadeType.ALL,
        orphanRemoval = true
    )
    private Address address;

    protected Employee() { }

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

    public void setAddress(Address address) {
        this.address = address;
        if (address != null) {
            address.setEmployee(this);
        }
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }
    public Address getAddress() { return address; }
}

3. Create the Address Entity

@MapsId makes the Address identifier use the same value as the associated Employee identifier. The address.employee_id column is both a primary key and a foreign key.

package com.example;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.MapsId;
import jakarta.persistence.OneToOne;

@Entity
public class Address {
    @Id
    private Long id;

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

    @MapsId
    @OneToOne
    @JoinColumn(name = "employee_id")
    private Employee employee;

    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 void setEmployee(Employee employee) {
        this.employee = employee;
    }

    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; }
}

Address is the owning side because it declares @JoinColumn. Employee's mappedBy = "employee" points to that field.

4. Configure Hibernate

Place hibernate.cfg.xml in src/main/resources and register both annotated 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;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.Employee"/>
    <mapping class="com.example.Address"/>
  </session-factory>
</hibernate-configuration>

Use Flyway, Liquibase, or another migration tool instead of create-drop in production.

5. Persist an Employee and Address

Set the relationship through the helper method so both sides of the bidirectional association are synchronized.

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

    Transaction transaction = session.beginTransaction();
    try {
        Employee employee = new Employee(
            "Ravi Malik", "ravi@example.com");
        Address address = new Address(
            "21 Lohia Nagar",
            "Ghaziabad",
            "Uttar Pradesh",
            "India",
            "201301");

        employee.setAddress(address);
        session.persist(employee);
        transaction.commit();
        System.out.println("Employee saved with address");
    } catch (RuntimeException exception) {
        transaction.rollback();
        throw exception;
    }
}

6. Fetch the Employee and Address

Use a fetch join when a screen needs the employee and address together.

Employee employee = session
    .createQuery(
        "select e from Employee e "
        + "join fetch e.address "
        + "where e.email = :email",
        Employee.class)
    .setParameter("email", "ravi@example.com")
    .getSingleResult();

Address address = employee.getAddress();
System.out.println(employee.getName());
System.out.println(address.getAddressLine());
System.out.println(address.getCity());

Keeping the session open while a lazy association is accessed prevents a LazyInitializationException. A fetch join makes the required data explicit.

One-to-One Mapping Alternatives

DesignWhen to use it
Shared primary key with @MapsIdThe dependent row cannot exist without the parent and should share its identifier.
Unique foreign keyThe dependent table has its own identifier but the foreign-key column must be unique.
Join tableThe relationship needs a separate table or both sides have independent lifecycle requirements.

A one-to-one association is not automatically enforced by Java annotations alone. Use primary-key, foreign-key, and unique constraints in the database to enforce cardinality.

Best Practices

  • Choose the owning side based on where the foreign key belongs.
  • Keep both sides synchronized with helper methods.
  • Use LAZY loading where supported and fetch deliberately.
  • Use orphanRemoval only when the dependent lifecycle belongs to the parent.
  • Do not include the relationship in equals() or hashCode().
  • Add database constraints to guarantee one-to-one cardinality.
  • Use migrations for production schema changes.
Result: the employee row is stored in employee, the address row is stored in address, and address.employee_id links the two with a shared primary key.