One To One Example | Spring Data JPA

Before jumping into @OneToOne examples, be sure to understand the basics of Spring Data JPA.

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

Author.java

@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @OneToOne(mappedBy = "author", cascade = CascadeType.ALL)
    @PrimaryKeyJoinColumn
    private Book book;
    //getters & setters
}
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    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
}
public class Book {
    @Id
    private Long id;
    @OneToOne
    @MapsId
    private Author author;
    //getters & setters
}

This creates a one-to-one relationship between an author and book table. A book has one author and an author has one book.

Using the @OneToOne annotation, Hibernate knows to create a one-to-one relationship between the two entities.

The mappedBy="author" option tells Hibernate that the owning side of the relationship is the book table. This means the book table will hold the foreign key reference to the author table.

The cascade = CascadeType.ALL tells Hibernate to propagate changes to all associated entities. If you delete an instance of Author it will delete the associated Book entity automatically.

The @PrimaryKeyJoinColumn tells Hibernate that the primary key for the author table will be used as the foreign key in the owning book table.

The @MapsId tells Hibernate that the primary key on the author table will be used as the primary key for the book table. Since the primary key is being reused, we don't need a to specify @GenderatedValue on the book table.

Running an example...

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

This generates the following in the database...

Book table

| author_id |
|    24     |  
|    24     |  

Author table

| id |
| 24 |
| 24 |

When retrieving entities from the database, associations will be eagerly loaded by default:

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

Why is this the BEST approach?

By utilizing the primary key of the author as both the primary key and foreign key of the book table, you achieve the same result with fewer keys. This saves operating costs as primary/foreign keys are typically indexed and loaded into memory for efficient lookups. You're achieving the same result with fewer keys via a unidirectional relationship.

2) @OneToOne Example Using Foreign Keys

Author.java

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

Book.java

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

With this approach, the author table is the owning side of the relationship. This means the author table will have the foreign key book_id. For these reasons, the mappedBy = is moved to the Book entity definition.

Since the primary key is no longer being reused in both tables, the Book entity must now generate its own id values. For these reasons, the @GeneratedValue annotation exists on both entity classes.

Running an example...

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

Note: Unlike the last example, we now save the author entity to create the associations in the database. Whichever side owns the relationship must be saved to create entries in both tables.. Otherwise, you will run into issues with transient objects not being saved and exceptions being thrown.

This generates the following in the database...

Book table

| id |
| 30 |
| 30 |

Author table

| id | book_id |
| 45 |   30    |
| 45 |   30    |

Notice how the author table now has 2 columns. This is because a foreign key is being used to create a bidirectional relationship between books and authors.

This example clearly demonstrates the advantage of the first approach as an extra book_id is needed to achieve the relationship.

3) @OneToOne Example Using a Join Table

Author.java

@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    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
}
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    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.AUTO)
    private Long id;
    @OneToOne(mappedBy = "book")
    private Author author;
    //getters & setters
}
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @OneToOne(mappedBy = "book")
    private Author author;
    //getters & setters
}

The only difference here is using the @JoinTable annotation. This specifies the join column representing the owner side of the relationship and the inverseJoinColumns representing the associated entity.

Running an example...

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

This generates the following in the database...

Book table

| id |
| 10 |
| 10 |

Author table

| id |
| 14 |
| 14 |

Author_Book table

| book_id | author_id |
|  10     |   14      | 
|  10     |   14      | 

The main difference with this example is the creation of the join table author_book.

Why would you ever use a join table?

A join table allows you to handle optional values better. The previous examples will produce null values if associated entities aren't set. By using a join table, only non-null related entities will get stored in the join table. This eliminates null values from being stored in optional relationships.

As an alternative, you can specify the optional = false option in the @OneToOne definition...

@OneToOne(optional = false)

This, however, will throw exceptions if the associated entity is not set before saving. A Join table gives you more flexibility in that you can allow null values AND not store them.

How @OneToOne Works (Behind the Scenes)

The Java Persistence API (JPA) defines how Java objects get persisted to database tables. ORM providers like Hibernate implement the JPA specification.

As an 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;
}
@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. They generate the necessary code to create database tables, relationships, foreign keys etc. so that you (as the developer) can simply use annotations to describe the behavior you want.

Spring Data JPA provides additional abstractions for working with Hibernate.

Confused? Check out Spring Data JPA vs Hibernate to understand the magic behind annotations like @OneToOne.

Your thoughts?

|

The first example is clearly the best approach and the example does a great job of showing you exactly why....

Many developers fail to understand that OneToOne relationships in Spring Data JPA can be achieved in several ways. Developers get annotation happy, creating ids and abusing the @OneToOne annotation. As the article points out, this can lead to unnecessary relationships being formed and unnecessary keys being created.

The cost can be enormous, especially as your data size grows. Utilizing the @MapsId when appropriate can save you a lot of space and manage the one to one relationship in a more straight forward way.

|

still confused on when / why you would use @PrimaryKeyJoinColumn???

|

great read. simple explanations. funny though how the first one is the only one worth exploring if you ask me!