Polymorphic association mappings of independent classes
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.
JPA and Hibernate make it very easy to model associations between entities. You can model associations between 2 concrete classes or model a polymorphic association to an inheritance hierarchy. These mappings are more than sufficient for almost all of your association mappings. But sometimes, you might want to model a polymorphic association to independent entity classes.
Unfortunately, JPA can’t model these kinds of associations without any workaround. But if you’re using Hibernate, you can easily model such associations using Hibernate’s proprietary @Any association mapping.
Modeling a polymorphic @Any and @ManyToAny associations
A polymorphic association mapping supported by the JPA specification requires your classes to belong to the same inheritance hierarchy. That’s not the case if you’re using Hibernate’s @Any mapping. But these classes still need to have something in common. All of them need to implement the same interface.
Independent entity mappings
In the example of this article, the interface that all entities implement is the Player interface. It’s a very basic interface that defines 2 getter methods to get the number of wins and losses of a player.
public interface Player { Integer getWins(); Integer getLoses(); }
The entity classes ChessPlayer and MonopolyPlayer implement the player interface. As you can see in the following code snippets, each defines its own, entirely independent mapping.
@Entity public class ChessPlayer implements Player { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "chess_player_seq") @SequenceGenerator(name = "chess_player_seq", sequenceName = "chess_player_seq", initialValue = 100) private Long id; private String firstName; private String lastName; private Integer wins; private Integer loses; ... }
@Entity public class MonopolyPlayer implements Player { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "monopoly_player_seq") @SequenceGenerator(name = "monopoly_player_seq", sequenceName = "monopoly_player_seq", initialValue = 100) private Long id; private String firstName; private String lastName; private Integer wins; private Integer loses; ... }
Using JPA’s standard association mappings, you could only reference each class in its own independent association mapping.
Using Hibernate’s proprietary @Any, @ManyToAny, and @AnyMetaDef annotations, you can model a polymorphic association to one or more entities that implement the same interface.
Defining Association Metadata
If your mapping can reference different types of entities, you need more than just the primary key value to persist your association. You also need to store the type of entity you’re referencing. This information is defined by a @AnyMetaDef annotation that you reference in your @Any and @ManyToAny association mapping. Let’s take a closer look at this first before use in different association mappings.
You could apply the @AnyMetaDef annotation to the attribute that represents your association. But it’s usually done on the class or package level. In the following code snippet, you can see a package-info.java file that defines that mapping for an entire package.
@AnyMetaDef(name = "player", metaType = "string", idType = "long", metaValues = { @MetaValue(value = "Chess", targetEntity = ChessPlayer.class), @MetaValue(value = "Monopoly", targetEntity = MonopolyPlayer.class) } ) @AnyMetaDef(name = "team", metaType = "string", idType = "long", metaValues = { @MetaValue(value = "Chess", targetEntity = ChessPlayer.class), @MetaValue(value = "Monopoly", targetEntity = MonopolyPlayer.class) }) package com.thorben.janssen.sample.model; import org.hibernate.annotations.AnyMetaDef; import org.hibernate.annotations.MetaValue;
The idType attribute specifies the type of the primary key of the entity classes that are part of this mapping.
The metaType and metaValue attributes work together. They define how Hibernate persists the entity type that this association element represents. The metaType specifies the type of the column in which the metaValues get persisted. The metaValue attribute contains an array of @MetaValue annotations. Each of these annotations specifies the mapping between an entity class and its identifier.
In this example, Hibernate stores the String Chess in the column player_type, and the value 1 in the column player_id to persists an association to a ChessPlayer entity with id 1.
Based on these definitions, you can then model your @Any and @ManyToAny associations
Defining an @Any association
I use a @Any association in my PlayerScore entity, which maps the score of a ChessPlayer or MonopolyPlayer.
@Entity public class PlayerScore { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "player_score_seq") @SequenceGenerator(name = "player_score_seq", sequenceName = "player_score_seq", initialValue = 100) private Long id; private Integer score; @Any(metaDef = "player", metaColumn = @Column(name = "player_type"), fetch = FetchType.LAZY) @JoinColumn(name = "player_id") private Player player; ... }
In contrast to JPA’s association mappings, an @Any association always requires a @JoinColumn annotation. It specifies the name of the column that contains the primary key of the associated entity object. In this example, it tells Hibernate that the table PlayerScore has the column player_id, which contains the primary key value of a Player.
As explained earlier, you also need to reference a @AnyMetaDef definition. You do that by providing the name of that definition as the value of the metaDef attribute. In this mapping, I reference the@AnyMetaDef with the name player. That’s the one we discussed in the previous section.
When you model an @Any association, please keep in mind that it’s a to-one association. Like all to-one associations, it gets fetched eagerly by default. This can introduce performance issues, and you should better set the FetchType to LAZY.
And that’s all you need to do to define your mapping. You can now use it in the same way as any other association mapping.
PlayerScore ps1 = em.find(PlayerScore.class, playerScore1.getId()); log.info("Get player ..."); ps1.getPlayer().getWins();
When you run this code, you can see in your log file that Hibernate executes 1 SQL SELECT statement to get the PlayerScore. Hibernate performs a 2nd SQL SELECT statement to get the record from the ChessPlayer table when using the modeled association to access the player.
13:27:47,690 DEBUG SQL:144 - select playerscor0_.id as id1_3_0_, playerscor0_.player_type as player_t2_3_0_, playerscor0_.player_id as player_i3_3_0_, playerscor0_.score as score4_3_0_ from PlayerScore playerscor0_ where playerscor0_.id=? 13:27:47,704 INFO TestSample:81 - Get player ... 13:27:47,705 DEBUG SQL:144 - select chessplaye0_.id as id1_0_0_, chessplaye0_.firstName as firstnam2_0_0_, chessplaye0_.lastName as lastname3_0_0_, chessplaye0_.loses as loses4_0_0_, chessplaye0_.wins as wins5_0_0_ from ChessPlayer chessplaye0_ where chessplaye0_.id=?
Defining a @ManyToAny association
If you want to model a to-many association, you can use a @ManyToAny annotation. In the following code snippet, I use that mapping to assign different types of players to a team. As you can see, the definition of such a mapping is very similar to the previous one.
@Entity public class MultiGameTeam { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "team_seq") @SequenceGenerator(name = "team_seq", sequenceName = "team_seq", initialValue = 100) private Long id; private String name; @ManyToAny(metaDef = "team", metaColumn = @Column(name = "player_type")) @JoinTable(name = "team_players", joinColumns = @JoinColumn(name = "team_id"), inverseJoinColumns = @JoinColumn(name = "player_id")) private List<Player> players; ... }
This association can reference multiple players. Because of that, you need to use a @JoinTable instead of a @JoinColumn annotation. That table contains the metaColumn defined by the @ManyToAny annotation and the 2 foreign key columns to reference the team and the player.
And you also need to reference a @AnyMetaDef definition. We already discussed that annotation in great detail in a previous section. So, I skip that here.
After you defined this mapping, you can use it in the same way as any other to-many association.
MultiGameTeam gameTeam = em.find(MultiGameTeam.class, team.getId()); log.info("Get the team"); assertThat(gameTeam.getPlayers().size()).isEqualTo(2); log.info("Check ChessPlayer"); assertThat(gameTeam.getPlayers().contains(chessPlayer)).isTrue(); log.info("Check MonopolyPlayer"); assertThat(gameTeam.getPlayers().contains(monopolyPlayer)).isTrue();
By default, all to-many associations are fetched lazily. So, when you get a MultiGameTeam entity from the database, Hibernate only selects the corresponding record from the MultiGameTeam table. When you then access the players attribute for the first time, Hibernate selects the association records from the JoinTable, before it executes a SQL SELECT statement for each player of the team.
13:40:31,341 DEBUG SQL:144 - select multigamet0_.id as id1_2_0_, multigamet0_.name as name2_2_0_ from MultiGameTeam multigamet0_ where multigamet0_.id=? 13:40:31,351 INFO TestSample:130 - Get team members 13:40:31,353 DEBUG SQL:144 - select players0_.team_id as team_id1_4_0_, players0_.player_type as player_t2_4_0_, players0_.player_id as player_i3_4_0_ from team_players players0_ where players0_.team_id=? 13:40:31,359 DEBUG SQL:144 - select chessplaye0_.id as id1_0_0_, chessplaye0_.firstName as firstnam2_0_0_, chessplaye0_.lastName as lastname3_0_0_, chessplaye0_.loses as loses4_0_0_, chessplaye0_.wins as wins5_0_0_ from ChessPlayer chessplaye0_ where chessplaye0_.id=? 13:40:31,363 DEBUG SQL:144 - select monopolypl0_.id as id1_1_0_, monopolypl0_.firstName as firstnam2_1_0_, monopolypl0_.lastName as lastname3_1_0_, monopolypl0_.loses as loses4_1_0_, monopolypl0_.wins as wins5_1_0_ from MonopolyPlayer monopolypl0_ where monopolypl0_.id=? 13:40:31,404 INFO TestSample:132 - Check ChessPlayer 13:40:31,405 INFO TestSample:134 - Check MonopolyPlayer
As you can see, fetching all players of a team can require a lot of statements. Because of that, this mapping is not the most efficient one. If possible, you should use a standard association mapping instead.
Summary
You can use JPA’s standard association mappings to reference another concrete entity class or reference an inheritance hierarchy. But JPA can’t model an association to multiple independent entity classes.
If you need such an association, you can use Hibernate’s @Any and @ManyToAny association mapping. It enables you to model an association to multiple entity classes that all implement the same interface. This mapping requires an additional @AnyMetaDef annotation that helps Hibernate map each association entry to a specific entity class and database table.