用于保存复杂游戏状态的 JPA 设计

JPA design for saving a complex game state

假设我有一个带有地图和单位的游戏,我想使用 JPA 2.2 在服务器上提供一个保存选项。这里用简化的类来表示:

@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;
}

所以每个方格上都可以有一个单位,每个单位里面都有一些士兵。

我有几个问题:

  1. 因为可以有很多游戏,所以UnitSoldierTileentityId不是唯一的。每场比赛可以有一个id为1的士兵。我不确定我是否 can/should 使用 @Embedded 或者我是否可以使用由实体 ID 和游戏 ID 组成的复杂 ID。

  2. Point 的适当注释是什么?它是不可变的。

  3. 地图的适当注释是什么?我想

     @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")
    

这是我的尝试,如果有更好的方法我想知道。以前我只是将整个状态序列化为一个 blob,但我希望能够查询更详细的状态。

What is the appropriate annotations for the maps?

最简单的方法是使用 @JoinColumn 而不是 @JoinTable(如果您打算制作 Game id 部分,我相信这是唯一可行的映射Units 或 Soldiers 的主键):

@OneToMany(cascadeType = ALL)
@JoinColumn(name = "game_id")
@MapKey(name = "entityId")
private Map<Long, Unit> units; 

以上映射将在 UNIT table 中创建一个 game_id 列。如果您坚持使用中间连接 tables,那么正确的映射应该是这样的:

@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.

因为Point没有身份,我建议你把它变成@Embeddable

how do i make the complex ID using the game id with the entity id?

首先,如果我是你,我真的会挑战自己,弄清楚可以有多少 SoldierUnitlong 是一个非常大的数字,正如您即将看到的那样,使用复合键映射会变得非常复杂。

但是,如果您最终决定需要复合密钥,则需要调整映射,将单向 @OneToMany 更改为双向密钥,并使用共享密钥。结果看起来像这样:

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;

}

请注意,Unit 不是协会的拥有方。这意味着对关联的任何更改都必须通过调整 Unit.game 属性 来完成。特别是,要将新的 Unit 添加到 Game,仅将 Unit 添加到 Game.units 地图是不够的 - 您还需要手动设置 Unit.game 属性 指向有问题的 Game

请注意,我尚未测试上述映射。虽然原则上它应该可以工作,但我从未尝试过将一侧的地图与另一侧的复合键结合使用,所以 - 不提供任何保证。