Mapping Java Entities for Persistence With Hibernate (Part 4)
Learn more about the additional ORM mapping features in Hibernate/JPA.
Join the DZone community and get the full member experience.
Join For FreeThis 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.
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
Published at DZone with permission of Mahmoud Anouti, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments