|

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 Maps, 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 Collections 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.

Author-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
	@JoinTable(
		name="BookAuthor",
		joinColumns={@JoinColumn(name="bookId", referencedColumnName="id")},
		inverseJoinColumns={@JoinColumn(name="authorId", referencedColumnName="id")})
	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:

  1. change the type of the attribute from List to Map and
  2. 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 a java.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
	@JoinTable(
		name="AuthorBookGroup",
		joinColumns={@JoinColumn(name="fk_author", referencedColumnName="id")},
		inverseJoinColumns={@JoinColumn(name="fk_group", referencedColumnName="id")})
	@MapKey(name = "title")
	private Map<String, Book> books = new HashMap<String, Book>();

	...
}

Working with Maps instead of Lists

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.

TypedQuery<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.

Author-Address-AddressType

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 Addresses 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:

  1. change the type of the attribute from List to Map
  2. 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 AuthorBook-association I showed you at the beginning of this post.

Most Books 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.

BookGroup

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(strategy = GenerationType.AUTO)
	@Column(name = "id", updatable = false, nullable = false)
	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
	@JoinTable(
		      name="AuthorBookGroup",
		      joinColumns={@JoinColumn(name="fk_author", referencedColumnName="id")},
		      inverseJoinColumns={@JoinColumn(name="fk_group", referencedColumnName="id")})
	@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 Books 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.

2 Comments

  1. Really helpful. Thanks.

    1. Avatar photo Thorben Janssen says:

      Thanks Thanh 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.