Hibernate does not delete orphans on OneToMany

2020-07-14 10:06发布

问题:

I have the following pretty simple one to many relations:

Team has a Set of players:

@Entity(name = "TEAM")
@Access(AccessType.PROPERTY)
public class Team{
    private Integer id;
    private String name;
    private Set<Player> players ;

    @Id
    @Column(name = "id")
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Column(name = "team_name")
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @OneToMany(cascade = {CascadeType.ALL},orphanRemoval=true)
    @JoinColumn(name = "TEAM_ID")
    public Set<Player> getPlayers() {
        return players;
    }

    public void setPlayers(Set<Player> players) {
        this.players = players;
    }       
}

And each player has a unique id & name.

@Entity(name = "PLAYER")
@Access(AccessType.PROPERTY)
public class Player implements Serializable{

    private int id;
    private String name;

    @Id
    @Column(name = "id")
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }

    @Column(name = "player_name")
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public boolean equals(Object obj) {
    return id == ((Player)obj).id;
    }
    @Override
    public int hashCode() {
        return id;
    }
}

I run a very simple code:

Team team = createTeam(3) // creates team with 3 players ids={1,2,3}
session.saveOrUpdate(team);
...

private Team createTeam(int players) {
    Team team = new Team();
    team.setName("Bears");
    team.setId(1);
    for(int i=1 ; i<=players; ++ i){
        Player player = new Player();
        player.setId(i);
        player.setName("Player"+i);
        team.addPlayer(player);
    }
    return team;
}

And I get the following as expected:

  • Hibernate: select team_.id, team_.team_name as team2_0_ from TEAM team_ where team_.id=?
  • Hibernate: select player_.id, player_.player_name as player2_1_ from PLAYER player_ where player_.id=?
  • Hibernate: select player_.id, player_.player_name as player2_1_ from PLAYER player_ where player_.id=?
  • Hibernate: select player_.id, player_.player_name as player2_1_ from PLAYER player_ where player_.id=?
  • Hibernate: insert into TEAM (team_name, id) values (?, ?)
  • Hibernate: insert into PLAYER (player_name, id) values (?, ?)
  • Hibernate: insert into PLAYER (player_name, id) values (?, ?)
  • Hibernate: insert into PLAYER (player_name, id) values (?, ?)
  • Hibernate: update PLAYER set TEAM_ID=? where id=? Hibernate: update PLAYER set TEAM_ID=? where id=? Hibernate: update PLAYER set TEAM_ID=? where id=?

Then later I do:

Team team = createTeam(2) // creates team with 2 player ids={1,2}
session.saveOrUpdate(team);

And expect the orphan players to be deleted but I get:

  • Hibernate: select team_.id, team_.team_name as team2_0_ from TEAM team_ where team_.id=?
  • Hibernate: select player_.id, player_.player_name as player2_1_ from PLAYER player_ where player_.id=?
  • Hibernate: select player_.id, player_.player_name as player2_1_ from PLAYER player_ where player_.id=?
  • Hibernate: update PLAYER set TEAM_ID=null where TEAM_ID=?
  • Hibernate: update PLAYER set TEAM_ID=? where id=?
  • Hibernate: update PLAYER set TEAM_ID=? where id=?

Which leaves the orphan player (id=3) disconnected but not deleted... Any ideas what I do wrong?

回答1:

If you want that the players are removed as delete-orphan, you need that the players loose the reference to the team and the save the team.

What you are doing is the following:

  • Create a new object team.
  • Feed the team with 3 players
  • Persist

After that, each player row will contain a FK to team (id=1).

Then the code creates a new team, with the same id, and feed 2 players and persist.

At that point there still will be a player in DB that references to team 1.

From my POV every different business object should have their own business key. If you want to overwrite the players of team 1, you should first retrieve team where id = 1, and then feed the players.

private Team createTeam(int players) {
    Team team = session.get(Team.class, 1);
    if (team == null) {
       team = new Team();
       team.setName("Bears");
       team.setId(1);
    }
    team.clearPlayers();

    for(int i=1 ; i<=players; ++ i){
        Player player = new Player();
        player.setId(i);
        player.setName("Player"+i);
        team.addPlayer(player);
    }
    return team;
}

// Team.java
private void clearPlayers() {
   players.clear();
}

BTW, another advice. Don't allow to modify your players directly, that can lead to HibernateErrors such us "Don't change the reference to a collection...". Instead of setPlayers(), add methods for addPlayer() and removePlayer()

private void adddPlayer(Player player) {
   player.setTeam(this);
   players.add(player);
}

private void removePlayer(Player player) {
   player.setTeam(null);
   players.remove(player);
}

Also, a collection is mutable, so make getPlayers() to return a non-modifiable collection:

private Set<Player> getPlayers() {
   return Collections.unmodifiableSet(players);
}

Hope this sheds some light :)



回答2:

You could use this tag: @org.hibernate.annotations.Cascade(value = org.hibernate.annotations.CascadeType.DELETE_ORPHAN). So you get:

@OneToMany(cascade = {CascadeType.ALL},orphanRemoval=true) @org.hibernate.annotations.Cascade(value = org.hibernate.annotations.CascadeType.DELETE_ORPHAN) @JoinColumn(name = "TEAM_ID")



回答3:

Add mappedBy attribute in relationship of both entities.

Add Team in Player.

// in Player.java
@ManyToOne(mappedBy="players")
private Team team;

And MappeedBy in Player.

//In Team.java
@OneToMany(cascade = {CascadeType.ALL},orphanRemoval=true,mappedBy="team")
    @JoinColumn(name = "TEAM_ID")
    public Set<Player> getPlayers() {
        return players;
    }

When you have 1-TO-M relationship , child should have a reference of its parent. Then hibernate internally uses parent's id as a foreign in child table.

Your child table would have these 3 columns :

id , player_name,team_id