Polymorphic association mappings of independent classes

By Thorben Janssen

Association Mapping

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.


Tags

Association Mapping


About the author

Thorben is an independent consultant, international speaker, and trainer specialized in solving Java persistence problems with JPA and Hibernate.
He is also the author of Amazon’s bestselling book Hibernate Tips - More than 70 solutions to common Hibernate problems.

Books and Courses

Coaching and Consulting

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.

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}