Independent Class Polymorphic Association Mappings


JPA and Hibernate greatly facilitate association models between entities. You can model associations between 2 concrete classes or model a polymorphic association with a 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 with independent feature classes.

Unfortunately, JPA cannot model these kinds of associations without a workaround. But if you are using Hibernate, you can easily model such associations using Hibernate proprietary software. @Any mapping of associations.

Model a polymorph @Any and @ManyToAny associations

A polymorphic association mapping supported by the JPA specification requires that your classes belong to the same inheritance hierarchy. This is not the case if you are using Hibernate @Any mapping. But these classes still need to have something in common. All must implement the same interface.

Independent entity mappings

In the example in this article, the interface that all entities implement is the Player interface. It is a very basic interface which defines 2 getter methods to obtain the number of wins and losses of a player.

public interface Player {
    Integer getWins();

    Integer getLoses();
}

Feature classes Chess player and MonopolyPlayer implement the player interface. As you can see in the following code snippets, each defines their own completely 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 Standard Association Mappings, you could only reference each class in its own independent association mapping.

Use the Hibernate owner @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 the primary key value to keep your association. You should also store the type of entity you are referencing. This information is defined by a @AnyMetaDef annotation that you refer to in your @Any and @ManyToAny mapping of associations. Let’s take a closer look before using it in different association mappings.

You could apply the @AnyMetaDef annotation to the attribute that represents your association. But this is usually done at the class or package level. In the following code snippet, you can see a info-package.java which defines this 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 primary key feature classes that are part of this mapping.

the metaType and metaValue the attributes work together. They define how Hibernate maintains the type of entity that this association element represents. the metaType specifies the type of the column in which the metaValues get persisted. the metaValue The 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 persist an association with a Chess player entity with identifier 1.

Based on these definitions, you can then model your @Any and @ManyToAny associations

Define a @Any association

i use a @Any association in my PlayerScore entity, which maps the score of a Chess player 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;

    ...
}

Unlike JPA association mappings, a @Any the 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 at the column player_id, which contains the primary key value of a Player.

As explained previously, you should also refer to a @AnyMetaDef definition. You do this by supplying the name of this definition as the value of the metaDef attribute. In this mapping, I refer to the@AnyMetaDef with the name player. This is the one we talked about in the previous section.

When you model a @Any association, please keep in mind that this is a one-to-one association. Like all to-one associations, it is fetched impatiently by default. This can introduce performance issues, and you should better define the Recovery type at 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 executes a second SQL SELECT statement to get the record of the Chess player table when using the modeled association to access the drive.

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=?

Define a @ManyToAny association

If you want to model a too much association, you can use a @ManyToAny annotation. In the following code snippet, I use this 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 several players. For this reason, you must use a @JoinTable instead of a @JoinColumn annotation. This table contains the metacolumn defined by the @ManyToAny the annotation and the 2 foreign key columns to reference the team and the player.

And you must also reference a @AnyMetaDef definition. We have already discussed this annotation in detail in a previous section. So, I’m skipping that here.

After you define this mapping, you can use it in the same way as any other multi-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 lazily fetched. So when you get a Multi-game team database entity, Hibernate selects only the corresponding record in the database Multi-game team board. When you next access the players attribute for the first time, Hibernate selects the association records in the JoinTable, before executing an SQL SELECT statement for each player on 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, picking up all the players on a team can require a lot of declarations. Therefore, this mapping is not the most efficient. If possible, you should use a standard association mapping instead.

summary

You can use the JPA standard association mappings to reference another concrete entity class or reference an inheritance hierarchy. But JPA cannot model an association with multiple independent feature classes.

If you need such association, you can use Hibernate @Any and @ManyToAny mapping of associations. It allows you to model an association to multiple feature classes that all implement the same interface. This mapping requires additional @AnyMetaDef annotation that helps Hibernate map each association entry to a specific entity class and database table.



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *