JPA - One-To-One associations explained
In JPA (Java Persistence API), a one-to-one association is a type of relationship between two entities where each instance of one entity is associated with exactly one instance of another entity. This relationship is represented using the @OneToOne
annotation.
Key Points to Understand
- Cardinality:
- In a one-to-one relationship, each entity has a single corresponding entity. For example, if you have a
Person
entity and aPassport
entity, eachPerson
can have only onePassport
, and eachPassport
can be assigned to only onePerson
.
- In a one-to-one relationship, each entity has a single corresponding entity. For example, if you have a
- Owning vs. Non-Owning Side:
- Owning Side: The entity that contains the foreign key is considered the owning side of the relationship. This entity’s table will have the column that refers to the primary key of the other entity.
- Non-Owning (Inverse) Side: The other entity that does not contain the foreign key. This entity maps the relationship using the
mappedBy
attribute in the@OneToOne
annotation.
- Bidirectional and Unidirectional Relationships:
- Unidirectional: Only one entity is aware of the relationship. This means only one entity has the
@OneToOne
annotation, and the other entity has no reference to the relationship. - Bidirectional: Both entities are aware of the relationship. Each entity will have a reference to the other entity, and both will use the
@OneToOne
annotation.
- Unidirectional: Only one entity is aware of the relationship. This means only one entity has the
- Cascade Type:
- Cascade operations can be applied to propagate operations (like persist, remove) from one entity to another. This is often used in one-to-one relationships to manage both entities together.
- Fetch Type:
- By default, one-to-one relationships use
FetchType.EAGER
, meaning the related entity is fetched immediately along with the owner entity. However, you can change this toFetchType.LAZY
if you want to load the related entity on demand.
- By default, one-to-one relationships use
Example
Let’s consider an example where we have two entities: Person
and Passport
.
We wille be using the @MapsId
annotation, where a Person
entity has a one-to-one relationship with a Passport
entity.
Explanation of @MapsId
- The
@MapsId
annotation maps the primary key of the child (Passport
) to the primary key of the parent (Person
). - This enforces that both entities share the same primary key.
- The
Passport
entity doesn’t have its own generated ID but instead inherits the ID fromPerson
.
Here’s the same JPA One-to-One relationship using @MapsId
, but now with Lombok to simplify the entities. This reduces boilerplate code by automatically generating constructors, getters, setters, toString()
, equals()
, and hashCode()
.
Person.java
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "person")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString(exclude = "passport") // Avoid circular reference in toString()
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToOne(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true)
private Passport passport;
public Person(String name) {
this.name = name;
}
public void setPassport(Passport passport) {
this.passport = passport;
passport.setPerson(this); // Maintain bidirectional relationship
}
}
Explanation
- JPA Annotations
@Entity
: Marks this class as a JPA entity.@Table(name = "person")
: Maps this entity to the person table in the database.@Id
: Marks theid
field as the primary key.@GeneratedValue(strategy = GenerationType.IDENTITY)
:- Uses the database’s identity column for automatic primary key generation.
- Suitable for databases like MySQL and PostgreSQL.
@OneToOne(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true)
:- Declares a one-to-one relationship with
Passport
. mappedBy = "person"
: Theperson
field inPassport
owns the relationship.cascade = CascadeType.ALL
: Any operations (persist, remove, merge) onPerson
cascade toPassport
.orphanRemoval = true
: If aPerson
loses itsPassport
, thePassport
is automatically deleted.
- Declares a one-to-one relationship with
- Lombok Annotations
@Getter @Setter
: Automatically generates getter and setter methods.@NoArgsConstructor
: Generates a default constructor.@AllArgsConstructor
: Generates a constructor with all fields.@ToString(exclude = "passport")
: Prevents infinite recursion intoString()
(sincePerson
andPassport
reference each other).
- Custom
setPassport()
Method- Ensures both entities maintain a bidirectional relationship.
passport.setPerson(this);
ensures that thePassport
correctly references itsPerson
.
Passport.java
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "passport")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString(exclude = "person") // Avoid circular reference in toString()
public class Passport {
@Id
private Long id; // Uses the same ID as the Person entity
private String passportNumber;
@OneToOne
@MapsId // Uses the same primary key as Person
@JoinColumn(name = "id")
private Person person;
public Passport(String passportNumber) {
this.passportNumber = passportNumber;
}
}
Explanation
- JPA Annotations
@Entity
: MarksPassport
as a JPA entity.@Table(name = "passport")
: Maps to the passport table.@Id private Long id;
: Uses the same primary key as Person.@OneToOne
:- Defines a one-to-one relationship with
Person
.
- Defines a one-to-one relationship with
@MapsId
: Mapsid
to the primary key ofPerson
.Passport
does not have its own ID.- Instead, it shares the same ID as its
Person
.
@JoinColumn(name = "id")
:- Specifies that the
id
column links to the primary key of Person.
- Specifies that the
- Lombok Annotations
@Getter @Setter
: Automatically generates getter and setter methods.@NoArgsConstructor
: Generates a default constructor.@AllArgsConstructor
: Generates a constructor with all fields.@ToString(exclude = "person")
: Prevents infinite recursion intoString()
.
3. Creating and Persisting Data
Main.java
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf = HibernateConfig.createEntityManagerFactory();
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
// Create Person
Person person = new Person("John Doe");
// Create Passport
Passport passport = new Passport("ABC123456");
// Establish relationship
person.setPassport(passport);
// Persist Person (Passport will be automatically persisted due to CascadeType.ALL)
em.persist(person);
tx.commit();
// Fetch and print data
Person retrievedPerson = em.find(Person.class, person.getId());
System.out.println("Person: " + retrievedPerson);
System.out.println("Passport Number: " + retrievedPerson.getPassport().getPassportNumber());
} catch (Exception e) {
if (tx != null && tx.isActive()) {
tx.rollback();
}
e.printStackTrace();
} finally {
em.close();
emf.close();
}
}
}
Explanation
- EntityManager Setup
EntityManagerFactory emf = HibernateConfig.createEntityManagerFactory();
EntityManager em = emf.createEntityManager();
- Manages database operations.
- Transaction Handling
tx.begin();
: Starts a new transaction.tx.commit();
: Commits changes to the database.- If an exception occurs,
tx.rollback();
is executed.
- Creating and Persisting Entities
- Creates a
Person
named"John Doe"
. - Creates a
Passport
with number"ABC123456"
. - Calls
person.setPassport(passport);
to establish the relationship. - Calls
em.persist(person);
to savePerson
(automatically persistsPassport
due to CascadeType.ALL).
- Creates a
- Retrieving and Printing Data
- Retrieves the
Person
from the database. - Prints
Person
and their associatedPassport
number.
- Retrieves the
4. Database Schema (Generated by Hibernate)
The generated schema will look like this:
Person Table
id | name |
---|---|
1 | John Doe |
Passport Table
id | passport_number |
---|---|
1 | ABC123456 |
- The
id
ofpassport
is the same as theid
ofperson
(due to@MapsId
). - This ensures a One-to-One relationship with shared primary keys.
Key Takeaways
- One-to-One Relationship:
@OneToOne(mappedBy = "person")
inPerson.java
.@OneToOne @MapsId
inPassport.java
to share the same primary key.
- Cascade & Orphan Removal:
CascadeType.ALL
: Automatically saves/deletes the associatedPassport
whenPerson
is modified.orphanRemoval = true
: If aPerson
is deleted or loses theirPassport
, thePassport
is also deleted.
- Lombok Simplifications:
@Getter
,@Setter
,@NoArgsConstructor
,@AllArgsConstructor
, and `@ToString