Blog

One To One Example | Spring Data JPA

Before jumping into @OneToOne examples, ensure you're familiar with the basics of Spring Data JPA.

Key Takeaways

  • @OneToOne can be implemented effectively using shared primary keys, foreign keys, or join tables.
  • Using a shared primary key is often more efficient by reducing extra keys.
  • Join tables provide flexibility for handling optional relationships without creating nulls.

1) @OneToOne Example Using Shared Primary Key (Best Approach)

Author.java

@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @OneToOne(mappedBy = "author", cascade = CascadeType.ALL)
    @PrimaryKeyJoinColumn
    private Book book;
    //getters & setters
}

Book.java

@Entity
public class Book {
    @Id
    private Long id;
    @OneToOne
    @MapsId
    private Author author;
    //getters & setters
}

This creates a one-to-one relationship between the author and book tables. A book has one author, and an author has one book.

The @OneToOne annotation facilitates a one-to-one relationship between two entities.

The mappedBy="author" tells Hibernate that the owning side of the relationship is the book table, which maintains the foreign key reference to the author table.

The cascade = CascadeType.ALL ensures changes are propagated to associated entities. Deleting an Author will delete its Book automatically.

The @PrimaryKeyJoinColumn indicates the primary key of author is used as the foreign key in the owning book table.

The @MapsId tells Hibernate to use the primary key from author as the primary key for book. This avoids the need for @GeneratedValue on the book table.

Running an example...

Author author = new Author();
Book book = new Book();
book.setAuthor(author);
bookRepo.save(book);

This generates the following in the database...

Book table

| author_id |
|    24     |

Author table

| id |
| 24 |

When retrieving entities, associations are eagerly loaded by default:

Author author = authorRepo.findById(24) // Returns both the author and its associated book in a single DB call
Book book = author.getBook();

Why is this the BEST approach?

Using the primary key of author as both the primary and foreign key of the book table achieves results with fewer keys. Fewer keys lower operating costs since keys are usually indexed and loaded into memory for efficient lookups. This approach efficiently uses unidirectional relationships.

2) @OneToOne Example Using Foreign Keys

Author.java

@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @OneToOne(cascade = CascadeType.ALL)
    private Book book;
    //getters & setters
}

Book.java

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @OneToOne(mappedBy = "book")
    private Author author;
    //getters & setters
}

Here, the author table is the owning side of the relationship, holding the foreign key book_id. Thus, mappedBy is set in the Book entity definition.

Since primary keys are not reused between tables, the Book entity needs its own id values, necessitating @GeneratedValue on both entities.

Running an example...

Author author = new Author();
Book book = new Book();
author.setBook(book);
authorRepo.save(author);

Note: Unlike the previous example, we save the author entity to establish associations in the database. The owning side must be saved to create entries in both tables.

This generates the following in the database...

Book table

| id |
| 30 |

Author table

| id | book_id |
| 45 |   30    |

Notice the author table has an extra column, book_id, to maintain bidirectional relationship, illustrating the first approach's advantage due to fewer keys.

3) @OneToOne Example Using a Join Table

Author.java

@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @OneToOne(cascade = CascadeType.ALL)
    @JoinTable(name = "author_book",
            joinColumns = {@JoinColumn(name = "author_id", referencedColumnName = "id")},
            inverseJoinColumns = {@JoinColumn(name = "book_id", referencedColumnName = "id")})
    private Book book;
    //getters & setters
}

Book.java

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @OneToOne(mappedBy = "book")
    private Author author;
    //getters & setters
}

The @JoinTable annotation specifies the join column for the owner side of the relationship and inverseJoinColumns for the associated entity.

Running an example...

Author author = new Author();
Book book = new Book();
author.setBook(book);
authorRepo.save(author);

This generates the following in the database...

Book table

| id |
| 10 |

Author table

| id |
| 14 |

Author_Book table

| book_id | author_id |
|   10    |    14     |

The significant difference here is the existence of the join table author_book.

Why use a join table?

A join table can handle optional values better. Previous approaches produce null values if associated entities aren't defined. With join tables, only non-null related entities are stored, eliminating unwanted nulls in optional relationships.

Alternatively, use optional = false in @OneToOne:

@OneToOne(optional = false)

This will throw exceptions if the associated entity isn't defined before saving. A join table allows nulls without storing them.

How @OneToOne Works (Behind the Scenes)

The Java Persistence API (JPA) defines how Java objects map to database tables. ORM providers like Hibernate implement these specifications.

For example, JPA provides the interface for the @OneToOne annotation:

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OneToOne {
    Class targetEntity() default void.class;
    CascadeType[] cascade() default {};
    FetchType fetch() default FetchType.EAGER;
    boolean optional() default true;
    String mappedBy() default "";
    boolean orphanRemoval() default false;
}

ORM providers like Hibernate implement these annotations at runtime, generating code to create database tables, relationships, and foreign keys. This allows developers to use annotations to define desired behavior easily.

Spring Data JPA offers additional abstractions for working with Hibernate.

Need more clarity? Check out Spring Data JPA vs Hibernate to delve into the workings behind annotations like @OneToOne.

FAQ

What is the advantage of using shared primary keys?

Shared primary keys reduce the number of keys needed, which can decrease system resources like memory used in indexing, making lookups faster and operations more efficient.

Why might someone use a join table instead of direct foreign keys?

A join table allows for flexible relationship handling, particularly beneficial for managing optional relationships, avoiding storing null values without restrictions or exceptions.

What's the default fetching strategy for @OneToOne associations?

The default fetching strategy for @OneToOne associations in JPA is EAGER fetching, meaning associated entities are loaded immediately with their parent entity.

Mastering the tech interviewWhat everyone is doing wrong in tech interviews