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:
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:
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 ...
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 ...
static factories with parameters is one good approach, I appreciate that.
ReplyDeleteAnd the mapping is very good to, awesome post.
Thank you.
From Brazil.
I have one question. In one book I've read about Serialization Proxies, where the class implements Serializable, and use static methods: writeReplace, and readResolve. Do you know something about this? Why nobody uses in Serialization examples like this?
ReplyDeleteThanks.