{{announcement.body}}
{{announcement.title}}

Mapping Java Entities for Persistence With Hibernate (Part 4)

DZone 's Guide to

Mapping Java Entities for Persistence With Hibernate (Part 4)

Learn more about the additional ORM mapping features in Hibernate/JPA.

· Java Zone ·
Free Resource

Mapping Java Entities for Persistence With Hibernate

Learn more about mapping Java entities for persistence with Hibernate.

This post explores additional Java ORM mapping features in Hibernate/JPA, focusing on entity association mapping. Make sure to check the earlier posts: part 1, part 2, and part 3.

You may also like:  Hibernate Mapping Many-to-One Using Annotations

Also as a reminder, Hibernate is a powerful and feature-rich library – these posts serve as an overview of how to use some of its features to map Java domain model classes to relational tables. There are other topics that can be looked up in the official documentation.

Mapping One-to-One Associations

One-to-one associations are used to model a single-value relationship from one entity to another. Let’s consider the Publisher entity and the Address embeddable type. We may want to treat Address as an entity rather than an embeddable, especially if another entity (say User) will reference an Address instance. In this case, Address now has its own table, and the association between Publisher and Address would be mapped as a one-to-one association between two independent entities. The mapping in Java code is shown below:

import javax.persistence.CascadeType;
import javax.persistence.OneToOne;

@Entity
public class Publisher {

    @Id
    @GeneratedValue(generator = "idGenerator")  // idGenerator is a GenericGenerator
    protected Long id;

    @Column(nullable = false)
    protected String name;

    @OneToOne(cascade = CascadeType.PERSIST)
    protected Address address;

    ...
}

@Entity
public class Address {

    @Id
    @GeneratedValue(generator = "idGenerator")
    private Long id;

    private String streetName;
    private String city;

    ...
}


The @OneToOne annotation is added to the Address field. In addition, the CascadeType.PERSIST is added to enable cascading of persist operations of Publisher to its associated Address instance. By default, Hibernate generates and uses a foreign key column to map the association. This is the generated DDL by Hibernate’s automatic schema creator:

create table Address (
   id bigint not null,
   city varchar(255),
   streetName varchar(255),
   primary key (id)
)

create table Publisher (
   id bigint not null,
   name varchar(255) not null,
   address_id bigint,
   primary key (id)
)

alter table Publisher 
   add constraint FKdqy55988yphy6x9dctrlfkcuk 
   foreign key (address_id) 
   references Address


Here’s an example that creates a Publisher, links it to an Address, and persists it:

try(Session session = sessionFactory.openSession()) {
    Transaction transaction = session.beginTransaction();

    Address address = new Address("Crows Nest", "New South Wales");

    Publisher publisher = new Publisher();
    publisher.setName("Allen & Unwin");
    publisher.setAddress(address);
    session.persist(publisher);
    transaction.commit();
}


We can customize the foreign key column’s properties by applying the @JoinColumn annotation on the associated Address field. For example, we can change the column name and make the association non-optional.

@Entity
public class Publisher {

    ...

    @OneToOne(cascade = CascadeType.PERSIST)
    @JoinColumn(name = "ADDR_ID", nullable = false)
    protected Address address;

    ...
}


Using a Join Table for One-to-One Associations

Instead of using a foreign key column in the table of an associated entity, Hibernate can use an intermediate join table, where each row contains the IDs of the associated instances. This has the advantage of avoiding a nullable additional column in an entity’s table: if a certain Publisher has a null Address reference, then the intermediate table will not have a row for it. In case an association is always non-optional (not null), then it might be better to simply use the default foreign key strategy instead of a join table.

To use a join table, apply the @JoinTable annotation as follows:

import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;

@Entity
public class Publisher {
    ...

    @OneToOne(cascade = CascadeType.PERSIST)
    @JoinTable(name = "PUBLISHER_ADDR",
               joinColumns = @JoinColumn(name = "PUBLISHER_ID"),
               inverseJoinColumns = @JoinColumn(name = "ADDR_ID",
                                                nullable = false,
                                                unique = true))
    protected Address address;

    ...
}


The name of the join table is required. There are two foreign keys in the join table, each referencing the primary tables. The PUBLISHER_ID column acts as the primary key column, and both columns are NOT NULL and unique, as shown in the generated DDL schema:

create table Address (
   id bigint not null,
   city varchar(255),
   streetName varchar(255),
   primary key (id)
) 

create table Publisher (
   id bigint not null,
   name varchar(255) not null,
   primary key (id)
) 

create table PUBLISHER_ADDR (
   ADDR_ID bigint not null,
   PUBLISHER_ID bigint not null,
   primary key (PUBLISHER_ID)
)

alter table PUBLISHER_ADDR 
   add constraint UK_kkvv49xwdf7gdjvesrnr39v1 unique (ADDR_ID)

alter table PUBLISHER_ADDR 
   add constraint FKck3ggfqkhjf9bu3k1w34ofatq 
   foreign key (ADDR_ID) 
   references Address

alter table PUBLISHER_ADDR 
   add constraint FKhsfqdekjgaf3duwnwsd0fof9y 
   foreign key (PUBLISHER_ID) 
   references Publisher


Making the One-to-One Association Bidirectional

To make the association bidirectional, the other side (non-owning side) uses a mappedBy element on the field:

@Entity
public class Address {

    @OneToOne(mappedBy = "address")
    private Publisher publisher;

    ...
}

Address address = new Address("Crows Nest", "New South Wales");

Publisher publisher = new Publisher();
publisher.setName("Allen & Unwin");
publisher.setAddress(address);

address.setPublisher(publisher);
session.persist(publisher);


One-to-Many: Mapping Lists With Their Elements Indices

In part 2, we actually mapped a List<Book> in the Author entity:

@Entity
public class Author {

    @OneToMany
    @JoinColumn(name = "AUTHOR_ID")
    protected List<Book> books = new ArrayList<>();

    ...
}


When persisting Book instances, and later fetching them, Hibernate internally uses a PersistentBag to maintain the collection. If you are not going to do in-place change in the list, the order is preserved (the PersistentBag internally uses a List).

However, the order of elements is not stored explicitly in the database, and therefore, is not guaranteed to be preserved when operating on the list. If the order of elements needs to be stored in the database, then we need to use the @OrderColumn annotation to mark an additional column which will contain the index of the Book instance within the list:

@Entity
public class Author {

    @OneToMany
    @JoinColumn(name = "AUTHOR_ID")
    @OrderColumn(name = "BOOK_INDEX", nullable = "false")
    protected List<Book> books = new ArrayList<>();

    ...
}


Storing the element order in the table has its drawbacks, mainly because Hibernate will execute several SQL statements. For example, removing an element from the list triggers a DELETE statement, in addition to multiple UPDATE statements to update the column index values of the elements located after the removed one. Furthermore, an application often has a requirement to view items in an order that is different than the default one. For example, we may want to view a list of books written by an author ordered by publishing date.

Using a Join Table for One-to-Many Associations

Similar to one-to-one associations, a one-to-many association can use a join table. The advantage is the same: avoiding null values in the foreign key column. The @JoinTable is used instead of @JoinColumn, where the join table and its columns are defined:

@Entity
public class Book {
    ...

    @ManyToOne
    @JoinTable(name = "BOOK_AUTHOR",
               joinColumns = @JoinColumn(name = "BOOK_ID"),
               inverseJoinColumns = @JoinColumn(name = "AUTHOR_ID", nullable = false))
    protected Author author;

    ...
}

@Entity
public class Author {
    ...

    @OneToMany(mappedBy = "author", cascade = CascadeType.PERSIST)
    protected Set<Book> books = new HashSet<>();

    ...
}


The intermediate join table looks similar to the one for the one-to-one example. The difference is that the column AUTHOR_ID is not unique because an author can have more than book:

create table Author (
    id bigint not null,
    birthDay date,
    name varchar(255) not null,
    primary key (id)
)

create table Book (
    id bigint not null,
    title varchar(255) not null,
    primary key (id)
)

create table BOOK_AUTHOR (
    AUTHOR_ID bigint not null,
    BOOK_ID bigint not null,
    primary key (BOOK_ID)
)

alter table BOOK_AUTHOR 
    add constraint FK78oepvclterucki39cv30xw8q 
    foreign key (AUTHOR_ID) 
    references Author

alter table BOOK_AUTHOR 
    add constraint FKqknbd4thdsna3pg8w0pm748u5 
    foreign key (BOOK_ID) 
    references Book


Using a Map for One-to-Many Associations

Another way to map a one-to-many association is by using a map. The key would contain the identifier of the target entity, while the value in the map entry would be the reference to the entity instance. Here’s how it would look like for our Book and Author entity classes:

import javax.persistence.MapKey;

@Entity
public class Author {
    ...

    @MapKey(name = "id")
    @OneToMany(mappedBy = "author")
    protected Map<Long, Book> books = new HashMap<>();

    ...
}

@Entity
public class Book {
    ...

    @ManyToOne
    @JoinColumn(name = "AUTHOR_ID", nullable = false)
    protected Author author;

    ...
}


The @MapKey specifies which property in the Book entity is the key to the map. In this case (and the default if name is omitted), the map key is the primary key of the associated Book entity. It can also be some other property that is expected to have a unique constraint, such as the title of the book.

Mapping Many-to-Many Associations

A book may be written by more than one author, so the relationship could be modeled as a many-to-many association. Both Book and Author entity classes would have a collection field. Building on the previous section, we can use a join table to implement this mapping.

import javax.persistence.ManyToMany;

@Entity
public class Book {
    ...

    @ManyToMany(cascade = CascadeType.PERSIST)
    @JoinTable(name = "BOOK_AUTHOR",
               joinColumns = @JoinColumn(name = "BOOK_ID"),
               inverseJoinColumns = @JoinColumn(name = "AUTHOR_ID"))
    protected Set<Author> authors = new HashSet<>();

    ...
}


We can also make the association bidirectional by mapping a books field in the Author class, with a mappedBy element:

@Entity
public class Author {
    ...

    @ManyToMany(mappedBy = "authors")
    protected Set<Book> books = new HashSet<>();

    ...
}


In the join table, the primary key is a composite of both columns BOOK_ID and AUTHOR_ID in order to satisfy the multiplicity of the many-to-many relationship: we can have the same author linked to many books, and vice versa. Both columns are also foreign key columns referencing the main entity tables:

create table Author (
    id bigint not null,
    birthDay date,
    name varchar(255) not null,
    primary key (id)
)

create table Book (
    id bigint not null,
    title varchar(255) not null,
    primary key (id)
)

create table BOOK_AUTHOR (
    BOOK_ID bigint not null,
    AUTHOR_ID bigint not null,
    primary key (BOOK_ID, AUTHOR_ID)
)

alter table BOOK_AUTHOR 
    add constraint FK78oepvclterucki39cv30xw8q 
    foreign key (AUTHOR_ID) 
    references Author

alter table BOOK_AUTHOR 
    add constraint FKqknbd4thdsna3pg8w0pm748u5 
    foreign key (BOOK_ID) 
    references Book


Using an Intermediate Entity Class

The join table can be explicitly modeled by an entity class, which may be useful if we want to include additional data on the link between the two entities, for example, a boolean to indicate whether an author was the first author or a contributor to a certain book. Using such an approach is, therefore, more flexible than using the @JoinTable annotation.

Here’s an example of what an intermediate entity can look like:

@Entity
@Immutable
public class BookAuthor {

    @Embeddable
    public static class Id implements Serializable {

        @Column(name = "BOOK_ID")
        private Long bookId;

        @Column(name = "AUTHOR_ID")
        private Long authorId;

        @Override
        public int hashCode() {
            return bookId.hashCode() + authorId.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (obj != null && obj instanceof Id) {
                Id other = (Id) obj;
                return this.authorId.equals(other.authorId) && this.bookId.equals(other.bookId);
            }
            return false;
        }

    }

    @EmbeddedId
    private Id id = new Id();

    @Column(name = "IS_CONTRIB")
    private boolean isContributor;

    @ManyToOne
    @JoinColumn(name = "BOOK_ID", insertable = false, updatable = false)
    private Book book;

    @ManyToOne
    @JoinColumn(name = "AUTHOR_ID", insertable = false, updatable = false)
    private Author author;

    public BookAuthor() {}

    public BookAuthor(Book book, Author author, boolean isContributor) {
        this.book = book;
        this.author = author;
        this.isContributor = isContributor;

        this.id.bookId = book.getId();
        this.id.authorId = author.getId();

        book.getAuthors().add(this);
        author.getBooks().add(this);
    }
}


This is an entity class as marked by @Entity. It is also marked immutable because instances of this class will not be modified. As seen in part 1, this tells Hibernate not to do dirty checking when synchronizing the instances with the database, which improves performance.

The identifier property of this entity is a composite key defined by an Embeddable class. Finally, there are two @ManyToOne associations to the Book and Author entities. The join columns (foreign key columns in the intermediate table) are defined with insertable = false, updatable = false; otherwise, Hibernate will throw an error because these columns are already mapped within the Embeddable ID class.

The intermediate table is similar to the one using @JoinTable, but with an additional column as mapped in the BookAuthor entity class:

create table BookAuthor (
    AUTHOR_ID bigint not null,
    BOOK_ID bigint not null,
    IS_CONTRIB boolean,
    primary key (AUTHOR_ID, BOOK_ID)
)


In the Book and Author classes, the association to BookAuthor can be mapped each with a @OneToMany relationship:

@Entity
public class Book {
    ...

    @OneToMany(mappedBy = "book")
    protected Set<BookAuthor> authors = new HashSet<>();

    ...
}

@Entity
public class Author {
    ...

    @OneToMany(mappedBy = "author")
    protected Set<BookAuthor> books = new HashSet<>();

    ...
}


Hope you enjoyed this demonstration! Let us know your thoughts and questions in the comments.

Further Reading

Hibernate Mapping

Hibernate Mapping Many-to-One Using Annotations

Top Five Courses to Learn Hibernate In-Depth

Topics:
java ,hibernate ,orm ,jpa

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}