How to map an association as a java.util.Map
Take your skills to the next level!
The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.
The java.util.List<SomeEntityClass>
is the most common representation of a to-many association with JPA and Hibernate. But is it also the one that you want to use in your domain model? Does it fit your use cases or do you need smarter access to the associated elements?
Let’s be honest, a simple java.util.List
is good enough in most cases. But from time to time, a java.util.Map
would make the implementation of your business logic so much easier.
So, why not use JPA’s and Hibernate’s mapping capabilities and map the association to a Map
? Only for the relationships that are used as Map
s, of course. The mapping definition requires just a few additional annotations.
Mapping simple key-value pairs
The basic mappings are pretty simple. JPA and Hibernate allow you to map a Collection
of elements or an entity association to a java.util.Map
. The key and value of the Map can be a basic type, an embeddable class or an entity.
You might have recognized that Collection
s are not supported. If you want to map your association to a Map<YourKeyClass, List<YourValueClass>>
, you need to use a small workaround that I show you at the end of this post.
But let’s take a look at some simple mapping first and discuss the mapping concept.
Represent a to-Many Association as a Map<String, EntityClass>
Let’s start with a standard mapping definition of a many-to-many association that uses a List
. The following diagram shows an example of a many-to-many association between the Author
and the Book
.
And you can see the standard mapping using a List
in the following code snippet. If you’re not familiar with the mapping of one-to-many or many-to-many associations, you should take a look at my Ultimate Guide to Association Mappings with JPA and Hibernate before you continue.
@Entity
public class Author {
@ManyToMany
private List<Book> books = new ArrayList<Book>();
...
}
If you want to represent the association with a Map
instead of the List
, you just have to do 2 things:
- change the type of the attribute from
List
toMap
and - define the database column you want to use as a map key.
Let’s say you want to use the title
column of the Book
table as the map key. The title is a String
attribute of the Book
. So, it’s a basic type that’s already mapped by your entities and you just need to reference it in a @MapKey
annotation.
If you don’t want to use one of the attributes of the associated entity, you can also use:
- a basic type with a
@MapKeyColumn
annotation - an enum with a
@MapKeyEnumerated
annotation - a
java.util.Date
or ajava.util.Calendar
with a@MapKeyTemporal
annotation
Hibernate will persist the map key in an additional database column.
But back to our example. When you apply the described changes, the mapping of the Author entity looks like this.
@Entity
public class Author {
@ManyToMany
@MapKey(name = "title")
private Map<String, Book> books = new HashMap<String, Book>();
...
}
Working with Map
instead of List
That’s all you need to do to define the mapping. You can now use the map in your domain model to add, retrieve or remove elements from the association.
Author a = new Author();
a.setFirstName("Thorben");
a.setLastName("Janssen");
em.persist(a);
Book b = new Book();
b.setTitle("Hibernate Tips");
b.setFormat(Format.PAPERBACK);
b.getAuthors().add(a);
em.persist(b);
a.getBooks().put(b.getTitle(), b);
You also have 2 options to use the association in JPQL query. You can simply join the 2 entities and use them as a regular many-to-many relationship.
TypedQuery<Author> q = em.createQuery("SELECT a FROM Author a JOIN a.books b WHERE b.title LIKE :title", Author.class);
q.setParameter("title", "%Hibernate Tips%");
a = q.getSingleResult();
Or you can use the KEY
function to reference the map key in your query.
TypedQuer<Author> q = em.createQuery("SELECT a FROM Author a JOIN a.books b WHERE KEY(b) LIKE :title ", Author.class);
q.setParameter("title", "%Hibernate Tips%");
a = q.getSingleResult();
Represent a Collection
of Embeddables as a Map<EnumClass, EntityClass>
The mapping for embeddable classes is pretty similar. The main difference is that you need to use the @ElementCollection
annotation instead of a @ManyToMany
annotation.
Let’s say you want to persist an author with her business and private address. In your domain model, you might model that with the 3 classes you can see in the following diagram.
The Author
class becomes an entity and you can implement the Address
class as an embedabble. And for this example, I don’t want to keep the Address
es in a simple ElementCollection
. I want to use a Map
with the AddressType
enum.
As in the previous example, you only need to do 2 things to use a Map
instead of a List
:
- change the type of the attribute from
List
toMap
- define the mapping of the map key
The second step even becomes optional if you only use the enum as a map key and not as an attribute of the embeddable. If you don’t provide any additional information, Hibernate uses a set of defaults to persist the map key in a column of the collection table.
I prefer to define the column name and the EnumType
Hibernate shall use to persist the enum. You can see an example for that in the following code snippet. The name
attribute of the @MapKeyColumn
annotation defines the name of the database column and the @MapKeyEnumerated
defines how Hibernate shall persist the enum.
@Entity
public class Author {
@ElementCollection
@MapKeyColumn(name = "address_type")
@MapKeyEnumerated(EnumType.STRING)
private Map<AddressType, Address> address = new HashMap<AddressType, Address>();
...
}
That’s all you have to do to represent a collection of embeddables as a java.util.Map
. You can now use it in the same way as I showed you in the previous example.
Mapping multiple values to the same key
As you’ve seen in the previous examples, JPA and Hibernate don’t support any Collection
type as a map value. That’s unfortunate because, in my experience, that is the most common use case.
But there is a small and easy workaround for these situations. You just need to introduce a wrapper entity that wraps the List
or Map
of entities.
Let’s extend the example of the Author
–Book
-association I showed you at the beginning of this post.
Most Book
s get published in different Formats
, e.g. as an ebook and as a paperback. The Book
has a unique ISBN for each Format
. So, you need 2 database records to persist the ebook and paperback version of a Book
.
But you can’t persist these 2 records with the mapping from our first example. The 2nd Book
entity would replace the 1st one, when you add it to the Map<String, Book> books
.
You can fix that by introducing an additional entity that wraps the List
of Book
entities.
The mapping definition of that entity is very simple. You just need a primary key and a one-to-many association to the Book
entity. I also introduced a title
for the BookGroup
which I want to use as the map key on the Author
entity.
@Entity
public class BookGroup {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "group")
private List<Book> books = new ArrayList<Book>();
private String title;
...
}
You can then model the association between the BookGroup
and the Author
entity as a Map
.
@Entity
public class Author {
@ManyToMany
@MapKey(name = "title")
private Map<String, BookGroup> bookGroups = new HashMap<String, BookGroup>();
...
}
That’s all you need to do to model an association as a Map with multiple values per map key. You can now use it in the same way as I showed you in the first example.
Please be aware, that this mapping creates overhead and redundancy. The BookGroup
entity gets mapped to a database table which we didn’t need in the previous examples. And I now use the title of the BookGroup
as the key of the Map<String, BookGroup> bookGroups
. The BookGroup
title
will be the same as the title
of all Book
s in the group.
Summary
As you’ve seen, JPA and Hibernate also allow you to represent an association as a java.util.Map
. That can be beneficial when you need more sophisticated access to the related entities. But please be aware, that the representation as a Map
introduces an overhead. You should, therefore, only use it, when you really need the Map
representation.
The mapping definition is pretty easy. You just define the association between your entities, use a Map
as the attribute type and define the map key with a @MapKey
, @MapKeyColumn
, @MapKeyEnumerated
or @MapKeyTemporal
annotation.
Really helpful. Thanks.
Thanks Thanh 🙂