Mapping Java Entities for Persistence With Hibernate (Part 2)
Learn more about mapping Java entities for persistence with Hibernate.
Join the DZone community and get the full member experience.
Join For FreeIn the previous post on mapping Java domain model classes using Hibernate and JPA, we visited some basic entity mapping topics, such as mapping identifiers, columns, and embedded types. In this part, we’ll review how to map collections in entities as well as basic entity associations.
You may also like: Java Persistence Done Right
Persisting Collections
JDK collections are heavily used in any complex enterprise application because they are naturally required to model the business domain. For example, in our Book
entity, we may want to define a set of tags that describe the genre of the book. A Set<String>
would be suitable because we don’t care about the order of these tags and there shouldn’t be duplicates:
import javax.persistence.CollectionTable;
import javax.persistence.ElementCollection;
import javax.persistence.JoinColumn;
@Entity
public class Book {
@ElementCollection
@CollectionTable(name = "CONTENT_TAG",
joinColumns = @JoinColumn(name = "BOOK_ID"))
@Column(name = "TAG", nullable = false)
protected Set<String> contentTags = new HashSet<>();
...
}
It is important to remember to initialize the set within the declaration statement to avoid any null references. The declaration type also has to be the interface Set
, not the implementation HashSet
. The @ElementCollection
is required to map this collection. The two other annotations specify metadata about the table that will store the collection. @CollectionTable
specifies a name for the table (CONTENT_TAG
) that will store the tags, along with a foreign key column BOOK_ID
via the @JoinColumn
in the collection table. Finally, the @Column
defines the name of the column storing the actual tag strings, and also marks it as non-nullable.
If we enable logging of the schema creation, Hibernate will issue the following DDL when creating the collection table:
create table CONTENT_TAG (
BOOK_ID bigint not null,
TAG varchar(255) not null,
primary key (BOOK_ID, TAG)
)
alter table CONTENT_TAG
add constraint FKitxsi0kpxsoexvnjlnlxmgci7
foreign key (BOOK_ID)
references Book
In other words, a new table CONTENT_TAG
is created, which references the Book
table via a foreign key BOOK_ID
and contains a column TAG
to store an element in the collection.
There are other types of collections that an entity may need to have such as a list or a map, but the way these are mapped is similar to the above (consult the documentation of Hibernate for more details). The differences are in how to specify the ordering in the case of ordered collections like a list, or how to map the key column in the case of a map.
Basic Entity Associations
So far, we mainly have one entity that is mapped, the Book
entity. Applications usually have many more entities, where some are associated with others. For example, every book should have an author and a publisher, and these warrant their own entities in our model: an Author
entity and a Publisher
entity. Let’s define them:
@Entity
public class Author {
@Id
@GeneratedValue(generator = "idGenerator")
@GenericGenerator(name = "idGenerator", strategy = "enhanced-sequence", parameters = {
@Parameter(name = SequenceStyleGenerator.SEQUENCE_PARAM, value = "BookSequence"),
@Parameter(name = SequenceStyleGenerator.INITIAL_PARAM, value = "100") })
protected Long id;
@Column(nullable = false)
protected String name;
@Temporal(TemporalType.DATE)
protected Date birthDay;
@Column(length = 30, nullable = false)
protected String country;
...
}
@Entity
public class Publisher {
@Id
@GeneratedValue(generator = "idGenerator")
@GenericGenerator(name = "idGenerator", strategy = "enhanced-sequence", parameters = {
@Parameter(name = SequenceStyleGenerator.SEQUENCE_PARAM, value = "BookSequence"),
@Parameter(name = SequenceStyleGenerator.INITIAL_PARAM, value = "100") })
protected Long id;
@Column(nullable = false)
protected String name;
protected Address address;
...
}
We used the enhanced-sequence
generator to generate the identifier values. The Author
also has a temporal field containing the date of birth, and the Publisher
embeds an Address
component. See part 1 for a quick review of these features.
Mapping a Many-to-One Association
An Author
should be associated with a Book
. An author can have many books written by him or her, while every book must have an author. On the Book
side, there is a many-to-one association to Author
. Let’s focus on this side first, which is the starting point in such associations. We introduce a field author
in the Book
class and map it with a @ManyToOne
annotation. Similarly, a field publisher
is defined to represent the association to a Publisher
:
import javax.persistence.ManyToOne;
@Entity
public class Book {
...
@ManyToOne
@JoinColumn(name = "AUTHOR_ID", nullable = false)
protected Author author;
@ManyToOne(optional = false) // equivalent to nullable = false
@JoinColumn(name = "PUBLISHER_ID")
protected Publisher publisher;
... // getters and setters
}
The resulting schema is shown in the following DDL:
create table Book (
id bigint not null,
publishingDate date,
title varchar(255) not null,
volumes integer not null,
AUTHOR_ID bigint not null,
PUBLISHER_ID bigint not null,
primary key (id)
)
alter table Book
add constraint FKe3rppuv3qa0j4ewpn52qfljn6
foreign key (AUTHOR_ID)
references Author
alter table Book
add constraint FKrb2njmkvio5mhe42empuaiphu
foreign key (PUBLISHER_ID)
references Publisher
So now, the Book
table has two foreign key columns AUTHOR_ID
and PUBLISHER_ID
, as specified in the @JoinColumn
annotations. They are also non-nullable, making the author
and publisher
fields required in a Book
object before being persisted in a session.
Bidirectional Association With One-to-Many
Each of these many-to-one associations (from Book
to Author
and from Book
to Publisher
) is so far a unidirectional association. The other side does not map an association to Book
, because it is optional to do so. We can now get the author of any loaded Book
instance by accessing its author
property. If we want to get all books written by an author, we can write a JPQL query and execute it. However, if this specific query is commonly needed, it may be a good idea to map the other side, for example by having a List<Book>
in the Author
class annotated with @OneToMany
. This way, Hibernate automatically executes select * from Book where AUTHOR_ID = ?
whenever we call author.getBooks()
:
import javax.persistence.OneToMany;
@Entity
public class Author {
...
@OneToMany(mappedBy = "author")
protected List<Book> books = new ArrayList<>();
... // getters and setters
public void addBook(Book book) {
this.books.add(book);
book.setAuthor(this);
}
}
The mappedBy
element in @OneToMany
specifies which field is the owner of this bidirectional association. It is the author
field in the Book
class. This fits naturally as an author “owns” their books. In terms of the DB schema, nothing changes: Hibernate relies on the foreign key column mapped earlier on the author
to construct the SQL that selects all books of a given author.
Two things are important to remember. First, whenever we load an Author
object from the database using Hibernate, its books
are not immediately fetched. Only when we access it (e.g. using a getter) will it send the SQL to fetch and load all its Book
instances. This is called lazy fetching and is the default behavior for one-to-many relationships. It can be switched to eager fetching if needed. Note that for many-to-one association (@ManyToOne
), the default is eager fetching.
Second, managing a bidirectional entity association requires that both sides be consistent. Whenever we set the author of a book using setAuthor()
, we should also add the book to the author’s list. A recommendation is to group this in a helper method, as shown above in addBook()
.
Unidirectional One-to-Many Association
Although it seems less likely to be used, we can also have the one-to-many association on one side. In this case, the mappedBy
element used above would not work because Book
no longer has an association to Author
. Instead, we use a @JoinColumn
to tell Hibernate which column to use in order to fetch Book
instances when given an author id:
@Entity
public class Author {
@OneToMany
@JoinColumn(name = "AUTHOR_ID")
protected List<Book> books = new ArrayList<>();
...
}
Hibernate would then use that column as a foreign key for retrieving all books belonging to an author:
create table Book (
id bigint not null,
publishingDate date,
title varchar(255) not null,
volumes integer not null,
PUBLISHER_ID bigint not null,
AUTHOR_ID bigint,
primary key (id)
)
create table Author (
id bigint not null,
birthDay date,
country varchar(30) not null,
name varchar(255) not null,
primary key (id)
)
alter table Book
add constraint FKe3rppuv3qa0j4ewpn52qfljn6
foreign key (AUTHOR_ID)
references Author
More on Collection and Association Mappings
This post illustrated simple examples of mapping collections and/or associations between entities. Of course, there many JDK collections supported by Hibernate, and other examples of entity associations which you can find in the Hibernate documentation.
Stay tuned for part 3…
Further Reading
Introduction to JPA Architecture
[DZone Refcard] Getting Started With JPA
Published at DZone with permission of Mahmoud Anouti, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments