Spring Hibernate:使用组合主键存储 n:m table 值时出现 EntityExistsException

Spring Hibernate: EntityExistsException when storing n:m table value with combiend Priamry Key

所以我的第二个 post。这次我在做我的一个激情项目,结果比我想象的要复杂得多,我再次需要一些帮助。

我有两个实体:Gamestate 和 User。

用户应该能够加入多个游戏(/gamestates)。游戏(/游戏状态)应该有很多人加入。因此,它表示为 N:M 关系。

根据谁加入以及他们加入的时间,他们应该具有不同的角色,从而赋予他们在应用程序中的不同权利。这意味着我需要与自定义字段的 N:M 关系,因此我必须自己对关系 table 进行建模。我就到此为止了。

抽象模型:

@EqualsAndHashCode
@Getter
@Setter
@ToString
public abstract class AbstractModel {
    @Id
    @GeneratedValue
    protected Long id;

    @NotNull
    protected String identifier;
}

用户

@Getter
@Setter
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class User extends AbstractModel{

    private String nickName;
    private UserRole role;

    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "user", orphanRemoval = true)
    private LoginInformation loginInformation;

    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @OneToMany(cascade = {CascadeType.PERSIST}, fetch = FetchType.LAZY, mappedBy = "gameState")
    private List<UserGameState> userGameStates = new ArrayList<>();

    //DTO Constructor
    public User(UserDTO userDTO){
        this.identifier = Optional.ofNullable(userDTO.getIdentifier())
                .orElse(UUID.randomUUID().toString());
        this.nickName = userDTO.getNickName() == null ? "": userDTO.getNickName();
        this.role = UserRole.valueOf(userDTO.getRole());

        this.loginInformation = null;
        if(userDTO.getLoginInformation() != null) {
            setLoginInformation(new LoginInformation(userDTO.getLoginInformation()));
        } else {
            setLoginInformation(new LoginInformation());
        }

        (userDTO.getUserGameStates() == null ? new ArrayList<GameStateDTO>() : userDTO.getUserGameStates())
                .stream()
                .map(x -> new UserGameState((UserGameStateDTO) x))
                .forEach(this::addUserGameState);
    }

游戏状态

@Getter
@Setter
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class GameState extends AbstractModel{
    private String name;
    private String description;
    private String image;

    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @OneToMany(cascade = {CascadeType.PERSIST}, fetch = FetchType.LAZY, mappedBy = "user")
    private List<UserGameState> userGameStates = new ArrayList<>();

    //DTO Constructor
    public GameState(GameStateDTO gameStateDTO){
        this.identifier = Optional.ofNullable(gameStateDTO.getIdentifier())
                .orElse(UUID.randomUUID().toString());
        this.name = gameStateDTO.getName() == null ? "": gameStateDTO.getName();
        this.description = gameStateDTO.getDescription() == null ? "": gameStateDTO.getDescription();
        this.image = gameStateDTO.getImage() == null ? "": gameStateDTO.getImage();

        (gameStateDTO.getUserGameStates() == null ? new ArrayList<UserDTO>() : gameStateDTO.getUserGameStates())
                .stream()
                .map(x -> new UserGameState((UserGameStateDTO) x))
                .forEach(this::addUserGameState);
    }
    //----------------------1:1 Relationship Methods----------------------
    //----------------------1:N Relationship Methods----------------------
    public void addUserGameState(UserGameState userGameState) {
        if (userGameStates.contains(userGameState)) {
            return;
        }
        userGameStates.add(userGameState);
        userGameState.setGameState(this);
    }

    public void removeUserGameState(UserGameState userGameState) {
        if (!userGameStates.contains(userGameState)) {
            return;
        }
        userGameState.setGameState(null);
        userGameStates.remove(userGameState);
    }
    //----------------------N:1 Relationship Methods----------------------
    //----------------------N:M Relationship Methods----------------------
}

UserGameSatet(自定义 N:M Table)

@Getter
@Setter
@Entity
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class UserGameState{

    @EmbeddedId
    private User_GameState_PK id;

    @ManyToOne(cascade = {CascadeType.PERSIST}, fetch = FetchType.LAZY)
    @MapsId("user_id")
    @JoinColumn(name = "USER_ID", insertable = false, updatable = false)
    private User user;

    @ManyToOne(cascade = {CascadeType.PERSIST}, fetch = FetchType.LAZY)
    @MapsId("gameState_id")
    @JoinColumn(name = "GAMESTATE_ID", insertable = false, updatable = false)
    private GameState gameState;

    //add Role later

    public UserGameState(User u, GameState gs) {
        // create primary key
        this.id = new User_GameState_PK(u.getId(), gs.getId());

        // initialize attributes
        setUser(u);
        setGameState(gs);

    }


    public UserGameState(UserGameStateDTO userGameStateDTO){
        //this.id =
        this.user = null;
        this.gameState = null;
    }



    //----------------------1:1 Relationship Methods----------------------
    //----------------------1:N Relationship Methods----------------------
    //----------------------N:1 Relationship Methods----------------------
    public void setUser(User user) {
        if (Objects.equals(this.user, user)) {
            return;
        }

        User oldUser = this.user;
        this.user = user;

        if (oldUser != null) {
            oldUser.removeUserGameState(this);
        }

        if (user != null) {
            user.addUserGameState(this);
        }
    }

    public void setGameState(GameState gameState) {
        if (Objects.equals(this.gameState, gameState)) {
            return;
        }

        GameState oldGameState = this.gameState;
        this.gameState = gameState;

        if (oldGameState != null) {
            oldGameState.removeUserGameState(this);
        }

        if (oldGameState != null) {
            oldGameState.addUserGameState(this);
        }
    }
    //----------------------N:M Relationship Methods----------------------
}

User_GameState_PK(组合键)

@Embeddable
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class User_GameState_PK implements Serializable {

    @Column(name = "USER_ID")
    private Long user_id;

    @Column(name = "GAMESTATE_ID")
    private Long gameState_id;

    public User_GameState_PK(long user_id, long gameState_id){
        this.user_id = user_id;
        this.gameState_id = gameState_id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;

        if (o == null || getClass() != o.getClass())
            return false;

        User_GameState_PK that = (User_GameState_PK) o;
        return Objects.equals(user_id, that.user_id) &&
                Objects.equals(gameState_id, that.gameState_id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(user_id, gameState_id);
    }
}

在我的服务中保存连接的方法

(GameState 和 User 都已经实例化,该方法获取两个对象的标识符,从数据库中检索它们并添加它们之间的关系。)

    public Optional<GameStateDTO> addUserToGameState(String identifierGS, String identifierU) {
        GameState gameState = gameStateRepo.findByIdentifier(identifierGS)
                .orElseThrow(() -> new IllegalArgumentException("GameState ID has no according GameState."));
        User user = userRepo.findByIdentifier(identifierU)
                .orElseThrow(() -> new IllegalArgumentException("User ID has no according User."));

        //Custom N:M Connection Part
        UserGameState connection = new UserGameState(user, gameState);
        userGameStateRepo.save(connection);

        return Optional.of(gameState)
                .map(m -> convertModelIntoDTO(m));
    }

我设法设置了 N:M table 及其组合键。我使用简单的 CRUD 路由对其进行了测试,它们有效。

接下来我尝试设置一些路线,以便人们可以实际加入游戏 (/gamestate),此时它会在保存时抛出以下异常。

javax.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session : [com.Astralis.backend.model.UserGameState#User_GameState_PK(user_id=1, gameState_id=7)]

在 Whosebug 上阅读了一些 post 之后,我尝试将 Cascadetype 更改为 .MERGE,这导致了这个异常。

javax.persistence.EntityNotFoundException: ...

真的我在这里迷路了,感觉如果我使用.PERSIST,Hibernate 抱怨它在保存关系时复制了自己。如果我将其更改为 .MERGE,它会抱怨该值一开始就不存在。

我非常感谢任何让我更接近解决方案的面包屑,因为事实证明这对项目来说是一个巨大的障碍,我已经尝试了我能想到的一切。

于是又找了几天终于解决了

为此,我首先使用指南中的数据结构和我项目的 service/controller 结构重新制作了指南的项目。测试它是否可行,正如它所做的那样,我刚刚开始将模型相互比较并尝试所有不同的可能性,以找出真正导致问题的原因。

使用的指南是这个:https://vladmihalcea.com/the-best-way-to-map-a-many-to-many-association-with-extra-columns-when-using-jpa-and-hibernate/

我有六个复制和粘贴(有点)错误导致 Hibernate 错误地将 table 列相互关联。这些是:

  1. 在用户中:
...

    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @OneToMany(
            cascade = {CascadeType.PERSIST},
            fetch = FetchType.LAZY,
            mappedBy = "user",// changed from gameState to user
            orphanRemoval = true
    )
    private List<UserGameState> userGameStates = new ArrayList<>();

...
  1. 在 GameState 中相反:
...

    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @OneToMany(cascade = {CascadeType.PERSIST},
            fetch = FetchType.LAZY,
            mappedBy = "gameState",// changed from user to gameState
            orphanRemoval = true)
    private List<UserGameState> userGameStates = new ArrayList<>();

...

3&4。 JoinColumn 注释是不必要的,似乎我将多个指南合并到一个项目中。这导致了更多问题:

...

    @ManyToOne(
            cascade = {CascadeType.PERSIST},
            fetch = FetchType.LAZY)
    @MapsId("user_id")
    //@JoinColumn(name = "USER_ID", insertable = false, updatable = false) //this one removed
    private User user;

    @ManyToOne(
            cascade = {CascadeType.PERSIST},
            fetch = FetchType.LAZY)
    @MapsId("gameState_id")
    //@JoinColumn(name = "GAMESTATE_ID", insertable = false, updatable = false) //this one removed
    private GameState gameState;

...

5&6。在 UserGameState 的“continuity keeper”方法中有两个小的复制和粘贴错误:

...


    public void setGameState(GameState gameState) {
        if (Objects.equals(this.gameState, gameState)) {
            return;
        }

        GameState oldGameState = this.gameState;
        this.gameState = gameState;

        if (oldGameState != null) {
            oldGameState.removeUserGameState(this);
        }

        //I copied the previous if block, and replaced the remove... with add...
        //But I didn't change the oldGameState to gameState.
        //This didn't throw any errors, and actually it still created the relations         properly, but I am pretty sure it would cause issues further down the line.
        if (gameState != null) {
            gameState.addUserGameState(this);
        }
    }
...

那么现在它是如何工作的: 和以前一样,当调用具有连接的 GameState 和 User 的标识符的路由时,将调用服务“addUserToGameState”,获取具有给定标识符的模型。

...
    public Optional<GameStateDTO> addUserToGameState(String identifierGS, String identifierU) {
        GameState gameState = gameStateRepo.findByIdentifier(identifierGS)
                .orElseThrow(() -> new IllegalArgumentException("GameState ID has no according GameState."));
        User user = userRepo.findByIdentifier(identifierU)
                .orElseThrow(() -> new IllegalArgumentException("User ID has no according User."));

        //Custom N:M Connection Part
        UserGameState connection = new UserGameState(user, gameState);

        return Optional.of(gameState)
                .map(m -> convertModelIntoDTO(m));
    }
...

之后调用 UserGameState 构造器,它设置和创建组合键并调用相关 User/GameState 字段的 setter 方法。

...
    public UserGameState(User u, GameState gs) {
        // create primary key
        this.id = new User_GameState_PK(u.getId(), gs.getId());

        // initialize attributes
        setUser(u);
        setGameState(gs);

    }
...

我写 setter 的方式是,它们同时检查添加的模型是否存在关系一致性问题,并根据它们是否被新编辑或替换来调整它们的字段。

...
    public void setUser(User user) {
        if (Objects.equals(this.user, user)) {
            return;
        }

        User oldUser = this.user;
        this.user = user;

        if (oldUser != null) {
            oldUser.removeUserGameState(this);
        }

        if (user != null) {
            user.addUserGameState(this);
        }
    }

    public void setGameState(GameState gameState) {
        if (Objects.equals(this.gameState, gameState)) {
            return;
        }

        GameState oldGameState = this.gameState;
        this.gameState = gameState;

        if (oldGameState != null) {
            oldGameState.removeUserGameState(this);
        }

        if (gameState != null) {//copy paste error
            gameState.addUserGameState(this);
        }
    }
...