Suppose I have a game with a map and units on it and I want to provide a save option on the server using JPA 2.2. Here are simplified classes to represent it:
@Entity
class GameState {
@Id
long gameId;
Map<Point, Tile> map;
Map<Long, Unit> units; // maps from the id to the unit
Map<Long, Soldier> soldiers; // maps from the id to the soldier
}
@Entity
class Tile {
@Id
long entityId;
@OneToOne(mappedBy = "tile")
Unit unit; // null if no unit on this tile
}
@Entity
class Unit {
@Id
long entityId;
@OneToOne
Tile tile; // null if the unit is not on a tile
@OneToMany(mappedBy = "unit", cascade = CascadeType.ALL, orphanRemoval = true)
List<Soldier> soldiers;
}
@Entity
class Soldier {
@id
long entityId;
@ManyToOne(fetch = FetchType.LAZY)
Unit unit;
int health;
}
final class Point {
final int x, y;
}
so each tile can have a unit on it, and each unit has some soldiers in it.
I have several issues:
Since there can be many games, the entityId
of Unit
Soldier
and Tile
is not unique. There can be a soldier with id of 1 in each game. I'm not sure if i can/should use @Embedded
or if I can use a complex id made of the entity id and the game id.
What is the appropriate annotations for Point
? It's immutable.
What is the appropriate annotations for the maps? I thought something like
@OneToMany(cascade = CascadeType.ALL)
@JoinTable(name = "unit_mapping",
joinColumns = {@JoinColumn(name = "unit_id", referencedColumnName = "entityId")},
inverseJoinColumns = {@JoinColumn(name = "unit_id", referencedColumnName = "entityId")})
@MapKey(name = "id")
This is my attempt, but if there is a better approach I'd like to know. Previously I just serialized the whole state as a blob, but I want to be able to query finer details of the state.
What is the appropriate annotations for the maps?
The simplest would be to use a @JoinColumn
instead of a @JoinTable
(and I believe it is the only feasible mapping if you're planning on making the Game
id part of the primary key for Unit
s or Soldier
s):
@OneToMany(cascadeType = ALL)
@JoinColumn(name = "game_id")
@MapKey(name = "entityId")
private Map<Long, Unit> units;
The above mapping will create a game_id
column inside the UNIT
table. If you insist on using intermediary join tables instead, then the correct mapping would be sth like:
@OneToMany(cascadeType = ALL)
@JoinTable(name = "unit_mapping",
joinColumns = @JoinColumn(name = "game_id"),
inverseJoinColumns = @JoinColumn(name = "unit_id")
)
@MapKey(name = "entityId")
private Map<Long, Unit> units;
What is the appropriate annotations for Point? It's immutable.
Since Point
does not have an identity, I'd suggest you make it an @Embeddable
how do i make the complex ID using the game id with the entity id?
First of all, If I were you, I'd really challenge myself to figure out how many Soldier
s and Unit
s there can be. A long
is a really huuuuuge number, and as you're about to see, the mapping gets pretty complicated using the composite key.
However, if you decide you'll need a composite key after all, you'll need to adjust the mapping, changing the unidirectional @OneToMany
to a bidirectional one, with a shared key. The result will look sth like this:
public class UnitKey implements Serializable {
private long entityId;
private long gameId;
//getters, setters, equals, hashCode
}
@Entity
@IdClass(UnitKey.class) // in principle, you could use an EmbeddedId instead, but IdClass is a little more convenient with PKs made partly of FKs IMHO
public class Unit {
@Id
private long entityId;
@JoinColumn(name = "game_id")
@MapsId("gameId")
private Game game;
}
@Entity
public class Game {
@OneToMany(mappedBy = "game", cascadeType = ALL)
@MapKey(name = "entityId")
private Map<Long, Unit> units;
}
Note that Unit
is not the owning side of the association. This means that any changes to the association have to be done by adjusting the Unit.game
property. In particular, to add a new Unit
, to a Game
, it is not enough to add a Unit
to the Game.units
map - you also need to manually set the Unit.game
property to point to the Game
in question.
Note that I haven't tested the above mapping. Though it should work in principle, I've never tried combining a map on one side with a composite key on the other, so - no guarantees given.