In this post we will take a look at couple of ways how many-to-many relationships, which have additional information, can be mapped to entities using JPA@ManyToMany annotation cannot be applied in such cases as it does not allow to map additional information from the join table.

To show these approaches let's consider such small film rental database schema:


We have three main entity tables - actor, film, and customer. Where film_to_actor as well as rental can be called join tables with additional information. Actors are playing in the films as characters, and in our case an actor can play only one character in one film. This is represented by film_to_actor table. A customer can rent films, and he/she is not limited to one rent of a movie. Hence the rental table. Notice the difference between the rental and film_to_actor tables. film_to_actor has a composite key built from two foreign keys (film_id, actor_id), whereas rental has it's own id (as well as uuid similar to other main entities). Such difference is based on the business logic that we described earlier, as a pair of actor and film is unique we don't need additional id column, as for the rentals a pair of customer and film is not unique so in this case a separate id is added. This will help us show different mapping approaches.

Using @ElementCollection and Map for mapping many-to-many relationship with additional information

First thing first - we need to create a movie! After all the work on the film is finished we need to store it in the database somehow. Mapping actors and films is easy. An example of such mapping can be next:
That was easy enough, now what about a relation between films and actors? Well as, in our case, one actor can play one role in the film and actor identity defines which role it is, a Map is just asking to be used. An actor would serve as a map key and actor's role would be a value. Let's add such map to film entity as it is a logical place to store such information and map it with @ElementCollection.
Note, as there are three values that we need to map (main_character, first_name, last_name), we need to create an Embeddable to hold them all together at the same time:
In result we would have next class diagram:

If we would like to rather have information about in which films an actor played which character, we would place a Map in actor entity, where a film would be a map key and character information would be a map value.
We shouldn't have both such maps at the same time, mapping the same table using element collection. We should only either use it in Film or in Actor, but never in both. Let's give it a try and persist a film:
There are two things that we should look closer at here. First note that we need to persist actors separately, as we cannot benefit from cascading. But in a real application you would, most likely,  have actors persisted already. Next note that we are not exposing the Map to the users. Even though it's a good container to store key-value pairs it's much better and cleaner to provide methods on Film class to work with actors logic instead. Like in our test example we added a Film#addCharacter method:
which simply delegates the operation to the map. Similarly we can add a few more methods like this depending on the application needs:
This wraps the case of using element collections for mapping many-to-many relations with additional information. Let's move to another case.

Using @ManyToOne/@OneToMany and an entity to map many-to-many relationship with additional information

So we have a film and now we want to rent and watch it. If the film is good it might be rented multiple times, hence the difference in the schema. We need to be able to store a relation between same pairs of films and customers multiple times. Therefore Map will not suite our needs. Also it is worth mentioning that the renting relation itself (rental table), in some sense, represents an entity, which we might be interested in as an event of renting a film. For example we might like to report on how frequently some film is rented etc.
We already have film mapped, so we need only to map a customer and rental relationship. As we can see from our database diagram customer is basically the same as actor table, thus the mapping of basic fields for it will look the same. What is interesting here is how to map a rental relationship.
As you might already figured out we need to create a Rental entity first:
As you can see @ManyToOne mappings were used on film and customer. Now we can reference a collection of such rentals in the customer entity:
This results in next class diagram:

Let's rent a film now:
Similarly, we don't want to expose a collection of rentals "as is" from the customer entity. Instead business methods are provided in the Customer class to work with rented films.
It is worth mentioning that if we modeled the schema slightly differently we would also be able to use a Map and @OneToMany to map rentals. Assuming that at a given moment of time, a customer can rent just one film, we would be able to use a Map<LocalDateTime, Film>. In this case rental table would be designed as:

As mentioned in the comments in the SQL snippet - we could also get rid of the id column and create a composite key from all columns that remains.
And the mapping in the customer entity in such case would be:

Conclusions

A multiple possible ways to map many-to-many relationships using just JPA was presented as well as the way how to hide the plain relationship collection operations for users behind business methods on entities. Modeling the data is a very interesting topic and there are a lot of different ways you can do such kind of mappings. Choose the one that suites your needs the best.
Personally I would suggest using maps (either as element collections, or mapped as @OneToMany) in those cases where the relationship table is more representing a connection between other entities with some additional information, rather than being an entity by itself. In this case you will have a smaller amount of entity classes and your model will look cleaner to the end user, if you hide the work with maps behind entity's business methods. Another benefit of using maps in modeling this kind of relationships comes from it's support of unique keys by design. On the other hand if the many-to-many relationship feels more like an entity itself and might be used in application outside of the entities connected by this relationship - then model it like an entity.

All the code samples can be found on GitHub.

To be continued ...