Persistence top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Introduction

In Hibernate, we can represent one-to-many relationships in our Java beans by having one of our fields be a List.

In this quick tutorial, we’ll explore various ways of doing this with a Map instead.

2. Maps Are Different from Lists

Using a Map to represent a one-to-many relationship is different from a List because we have a key.

This key turns our entity relationship into a ternary association, where each key refers to a simple value or an embeddable object or an entity. Because of this, to use a Map, we’ll always need a join table to store the foreign key that references the parent entity – the key, and the value.

But this join table will be a bit different from other join tables in that the primary key won’t necessarily be foreign keys to the parent and the target. Instead, we’ll have the primary key be a composite of a foreign key to the parent and a column that is the key to our Map.

The key-value pair in the Map may be of two types: Value Type and Entity Type. In the following sections, we’ll look at the ways to represent these associations in Hibernate.

3. Using @MapKeyColumn

Let’s say we have an Order entity and we want to keep track of name and price of all the items in an order. So, we want to introduce a Map<String, Double> to Order which will map the item’s name to its price:

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue
    @Column(name = "id")
    private int id;

    @ElementCollection
    @CollectionTable(name = "order_item_mapping", 
      joinColumns = {@JoinColumn(name = "order_id", referencedColumnName = "id")})
    @MapKeyColumn(name = "item_name")
    @Column(name = "price")
    private Map<String, Double> itemPriceMap;

    // standard getters and setters
}

We need to indicate to Hibernate where to get the key and the value. For the key, we’ve used @MapKeyColumn, indicating that the Map‘s key is the item_name column of our join table, order_item_mapping. Similarly, @Column specifies that the Map’s value corresponds to the price column of the join table.

Also, itemPriceMap object is a value type map, thus we must use the @ElementCollection annotation.

In addition to basic value type objects, @Embeddable objects can also be used as the Map‘s values in a similar fashion.

4. Using @MapKey

As we all know, requirements changes over time — so, now, let’s say we need to store some more attributes of Item along with itemName and itemPrice:

@Entity
@Table(name = "item")
public class Item {
    @Id
    @GeneratedValue
    @Column(name = "id")
    private int id;

    @Column(name = "name")
    private String itemName;

    @Column(name = "price")
    private double itemPrice;

    @Column(name = "item_type")
    @Enumerated(EnumType.STRING)
    private ItemType itemType;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_on")
    private Date createdOn;
   
    // standard getters and setters
}

Accordingly, let’s change Map<String, Double> to Map<String, Item> in the Order entity class:

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue
    @Column(name = "id")
    private int id;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "order_item_mapping", 
      joinColumns = {@JoinColumn(name = "order_id", referencedColumnName = "id")},
      inverseJoinColumns = {@JoinColumn(name = "item_id", referencedColumnName = "id")})
    @MapKey(name = "itemName")
    private Map<String, Item> itemMap;

}

Note that this time, we’ll use the @MapKey annotation so that Hibernate will use Item#itemName as the map key column instead of introducing an additional column in the join table. So, in this case, the join table order_item_mapping doesn’t have a key column — instead, it refers to the Item‘s name.

This is in contrast to @MapKeyColumn. When we use @MapKeyColumn, the map key resides in the join table. This is the reason why we can’t define our entity mapping using both the annotations in conjunction.

Also, itemMap is an entity type map, therefore we have to annotate the relationship using @OneToMany or @ManyToMany.

5. Using @MapKeyEnumerated and @MapKeyTemporal

Whenever we specify an enum as the Map key, we use @MapKeyEnumerated. Similarly, for temporal values, @MapKeyTemporal is used. The behavior is quite similar to the standard @Enumerated and @Temporal annotations respectively.

By default, these are similar to @MapKeyColumn in that a key column will be created in the join table. If we want to reuse the value already stored in the persisted entity, we should additionally mark the field with @MapKey.

6. Using @MapKeyJoinColumn

Next, let’s say we also need to keep track of the seller of each item. One way we might do this is to add a Seller entity and tie that to our Item entity:

@Entity
@Table(name = "seller")
public class Seller {

    @Id
    @GeneratedValue
    @Column(name = "id")
    private int id;

    @Column(name = "name")
    private String sellerName;
   
    // standard getters and setters

}
@Entity
@Table(name = "item")
public class Item {
    @Id
    @GeneratedValue
    @Column(name = "id")
    private int id;

    @Column(name = "name")
    private String itemName;

    @Column(name = "price")
    private double itemPrice;

    @Column(name = "item_type")
    @Enumerated(EnumType.STRING)
    private ItemType itemType;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_on")
    private Date createdOn;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "seller_id")
    private Seller seller;
 
    // standard getters and setters
}

In this case, let’s assume our use-case is to group all Order‘s Items by Seller. Hence, let’s change Map<String, Item> to Map<Seller, Item>:

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue
    @Column(name = "id")
    private int id;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "order_item_mapping", 
      joinColumns = {@JoinColumn(name = "order_id", referencedColumnName = "id")},
      inverseJoinColumns = {@JoinColumn(name = "item_id", referencedColumnName = "id")})
    @MapKeyJoinColumn(name = "seller_id")
    private Map<Seller, Item> sellerItemMap;

    // standard getters and setters

}

We need to add @MapKeyJoinColumn to achieve this since that annotation allows Hibernate to keep the seller_id column (the map key) in the join table order_item_mapping along with the item_id column. So then, at the time of reading the data from the database, we can perform a GROUP BY operation easily.

7. Conclusion

In this article, we learned about the several ways of persisting Map in Hibernate depending upon the required mapping.

As always, the source code of this tutorial can be found over Github.

Persistence bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE