JPA实现tournaments实体后出现NullPointerException排查
实现Tournaments实体后抛出NullPointerException问题排查
问题背景
- 项目正在编写供Angular客户端测试使用的种子数据
- 新增Tournaments实体及关联关系前,
loadPlayerTeamData()、loadGamesData()方法运行完全正常,数据库可正常完成种子数据填充,无任何报错 - 新增Tournaments实体、关联映射及
loadTournamentsData()方法后,执行loadPlayerTeamDataSeed()时抛出空指针异常,同时咨询当前场景是否需要替换SQL为NoSQL
实体关联规则说明
Games实体
- 单场比赛仅关联1支客场队伍,单支队伍可关联多场客场比赛
- 单场比赛仅关联1支主场队伍,单支队伍可关联多场主场比赛
- 单场比赛仅关联1个赛事,单个赛事可关联多场比赛
- Games为关联关系非持有方,实体代码如下:
private Date date; private String location; @ManyToOne @JoinColumn(name="home_team_id") private Team home; @ManyToOne @JoinColumn(name="away_team_id") private Team away; @ManyToOne(optional = true) @JoinColumn(name="tournament_id") private Tournaments tournament;
Player实体
- 单个球员仅关联1条球员统计数据,单条统计数据仅归属1名球员,Player为该关联关系持有方
- 单个球员仅归属1支队伍,单支队伍可包含多名球员,Team为该关联关系持有方
- 实体代码如下:
@OneToOne(mappedBy = "player", cascade = CascadeType.ALL) @PrimaryKeyJoinColumn private PlayerStats stats; @ManyToOne @JoinColumn(name= "team_id") private Team team; private String name; @Enumerated(EnumType.STRING) private TeamRole position;
Team实体
- 单支队伍关联1条队伍统计数据,单条统计数据仅归属1支队伍,Team为该关联关系持有方
- 单支队伍可包含多名球员,单个球员仅归属1支队伍,Team为该关联关系持有方
- 单支队伍可关联多场主场比赛、多场客场比赛,单场比赛仅关联1支主场/客场队伍,Team为该关联关系持有方
- 单支队伍可参与多个赛事,单个赛事可包含多支参赛队伍,Tournaments为该关联关系持有方
- 控制台报错指向该实体的addPlayer方法,实体代码如下:
private String name; @OneToOne(mappedBy = "team", cascade = CascadeType.ALL) @PrimaryKeyJoinColumn private TeamStats stats; @OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true) private List<Player> players; @OneToMany( mappedBy = "home", cascade = CascadeType.ALL ) private List<Games> homeGames; @OneToMany( mappedBy= "away", cascade = CascadeType.ALL ) private List<Games> awayGames; //多对多关联赛事 @ManyToMany(mappedBy="numOfParticpants") private List<Tournaments> upcommingTournaments; public void addPlayer(Player player) { players.add(player);//控制台报错指向该行 player.setTeam(this); } public void removePlayer(Player player) { players.remove(player); player.setTeam(null); } public void addHomeGame(Games game) { homeGames.add(game); game.setHome(this); } public void removeHomeGame(Games game) { homeGames.remove(game); game.setHome(null); } public void addAwayGame(Games game) { awayGames.add(game); game.setAway(this); } public void removeAwayGame(Games game) { awayGames.remove(game); game.setAway(null); }
Tournaments实体
- 单个赛事可包含多支参赛队伍,单支队伍可参与多个赛事
- 单个赛事可关联多场比赛,单场比赛仅归属1个赛事;表演赛的tournament字段为null
- Tournament为所有关联关系的持有方,实体代码如下:
private String name; private Date scheduledDate; //多对多关联参赛队伍 @ManyToMany(cascade = {CascadeType.PERSIST,CascadeType.MERGE}) @JoinTable( name= "tournament_participants", joinColumns= @JoinColumn(name = "team_id"), inverseJoinColumns= @JoinColumn(name="tournament_id") ) private List <Team> numOfParticpants; @OneToMany(mappedBy="tournament", cascade = CascadeType.ALL) private List<Games> totalGames; private String location; private TournamentSize tournamentSize; public void addParticipant(Team team) { this.numOfParticpants.add(team); team.getUpcommingTournaments().add(this); } public void removeParticipant(Team team) { this.numOfParticpants.remove(team); team.getUpcommingTournaments().remove(this); } public void addGame(Games game) { this.totalGames.add(game); game.setTournament(this); } public void removeGame(Games game) { this.totalGames.remove(game); game.setTournament(null); }
报错信息
控制台抛出的异常栈如下:
Caused by: java.lang.NullPointerException: Cannot invoke "java.util.List.add(Object)" because "this.players" is null at com.unknown.server.model.Team.addPlayer(Team.java:105) ~[classes/:na] at com.unknown.server.dataseed.DataSeed.loadPlayerTeamData(DataSeed.java:88) ~[classes/:na] at com.unknown.server.dataseed.DataSeed.run(DataSeed.java:55) ~[classes/:na]
种子数据实现代码
loadPlayerTeamData方法
private void loadPlayerTeamData() { List<Team> teamList = teamService.getAllTeams().get(); Team team = new Team(); while(teamList.size() < 50) { //设置球员姓名和位置 Player player = new Player(); player.setName(faker.name().fullName()); player.setPosition(TeamRole.randomRole()); //设置球员统计数据 PlayerStats playerStats = new PlayerStats( player, faker.number().numberBetween(1, 20), faker.number().numberBetween(1, 20), faker.number().numberBetween(1, 20), faker.number().numberBetween(2, 40), faker.number().numberBetween(1, 20), faker.number().numberBetween(1, 20)); player.setStats(playerStats); //将球员加入队伍 team.addPlayer(player); //控制台报错指向该行 if(team.getPlayers().size() >= 10) { //计算队伍统计数据 int assists = 0, blocks = 0, points = 0, rebounds = 0, steals = 0, turnovers = 0; for(Player p: team.getPlayers()) { assists =assists+p.getStats().getAssist(); blocks= blocks+p.getStats().getBlocks(); points= points+p.getStats().getPoints(); rebounds= rebounds+p.getStats().getRebounds(); steals= steals+p.getStats().getRebounds(); turnovers= turnovers+p.getStats().getTurnovers(); } TeamStats teamStats = new TeamStats( rebounds/team.getPlayers().size(), assists/team.getPlayers().size(), blocks/team.getPlayers().size(), points/team.getPlayers().size(), steals/team.getPlayers().size(), turnovers/team.getPlayers().size()); team.setName(faker.pokemon().name()); team.setStats(teamStats); teamService.addTeam(team); team = new Team(); teamList = teamService.getAllTeams().get(); } } }
loadGamesData方法
//每支队伍生成3场客场、3场主场比赛 private void loadGamesData() { List<Games> games = new ArrayList<Games>(); List<Team> teams = teamService.getAllTeams().get(); while(teams.size() > 1) { Team t = teams.get(0); teams.remove(0); while(t.getHomeGames().size() < 2) { int a = teams.size() == 2 ? faker.number().numberBetween(teams.size()/2, teams.size()-1) : faker.number().numberBetween(0, teams.size()-1); Date d = faker.date().future(1, TimeUnit.DAYS); String location = faker.address().fullAddress(); Team opp = teams.get(a); Games game = new Games(d, location, t, opp, null); t.addHomeGame(game); opp.addAwayGame(game); games.add(game); } while(t.getAwayGames().size() < 2) { int a = teams.size() == 2 ? faker.number().numberBetween(0, teams.size()/2) : faker.number().numberBetween(0, teams.size()-1); Date d = faker.date().future(1, TimeUnit.DAYS); String location = faker.address().fullAddress(); Team opp = teams.get(a); Games game = new Games(d, location, opp, t, null); t.addAwayGame(game); opp.addHomeGame(game); games.add(game); } } for(Games g: games) { gamesService.addGame(g); } }
loadTournamentsData方法
private void loadTournamentsData() { List<Tournaments> tournies = new ArrayList<>(); List<Team> teams = teamService.getAllTeams().get(); List<Team> scheduled = new ArrayList<>(); while(tournies.size() < 10){ Tournaments tournie = new Tournaments(); String location = faker.address().fullAddress(); tournie.setName(faker.pokemon().name()); tournie.setLocation(location); tournie.setScheduledDate(faker.date().future(50, TimeUnit.DAYS)); //生成参赛队伍列表并加入赛事 for(int i = 0; i < TournamentSize.fixedSize(TournamentSize.EIGHT); i++) { Team team = teams.get(faker.number().numberBetween(0, teams.size()-1)); scheduled.add(team); teams.remove(team); tournie.addParticipant(team); } //生成赛事对阵比赛 int opp1 = 0, opp2 = scheduled.size()-1; while(opp1 < opp2) { Team home = scheduled.get(opp1); Team away = scheduled.get(opp2); Games game = new Games( faker.date().future(52, TimeUnit.DAYS), location, home, away, null ); tournie.addGame(game); opp1++; opp2--; } tournies.add(tournie); scheduled.clear(); } for(Tournaments t: tournies) { tournamentsService.addTournament(t); } }
问题根因与修复方案
直接原因
空指针原因非常明确:你用new Team()创建实例时,类中声明的所有集合类型字段(players/homeGames/awayGames/upcommingTournaments)都没有做初始化,默认值为null,调用addPlayer方法时直接执行players.add(player)必然触发空指针。
之前代码能正常运行只是巧合:要么是之前JPA的字节码增强逻辑自动给集合字段做了初始化,要么是之前的代码流程刚好没触发这个边界情况。新增Tournaments实体后,JPA加载实体映射的逻辑发生变化,这个隐藏了很久的基础疏漏就暴露了,和Tournaments本身的业务逻辑没有直接关系。
另外你Tournaments实体里的numOfParticpants、totalGames集合也没做初始化,后续调用addParticipant、addGame方法时会触发一模一样的空指针,属于同类隐患。
修复方式
给两个实体里所有集合类型的字段加上默认初始化即可:
//Team实体集合字段初始化示例 @OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true) private List<Player> players = new ArrayList<>(); @OneToMany( mappedBy = "home", cascade = CascadeType.ALL ) private List<Games> homeGames = new ArrayList<>(); @OneToMany( mappedBy= "away", cascade = CascadeType.ALL ) private List<Games> awayGames = new ArrayList<>(); @ManyToMany(mappedBy="numOfParticpants") private List<Tournaments> upcommingTournaments = new ArrayList<>();
//Tournaments实体集合字段初始化示例 @ManyToMany(cascade = {CascadeType.PERSIST,CascadeType.MERGE}) @JoinTable( name= "tournament_participants", joinColumns= @JoinColumn(name = "team_id"), inverseJoinColumns= @JoinColumn(name="tournament_id") ) private List <Team> numOfParticpants = new ArrayList<>(); @OneToMany(mappedBy="tournament", cascade = CascadeType.ALL) private List<Games> totalGames = new ArrayList<>();
关于数据库选型
完全不需要替换为NoSQL。当前是典型的强结构化实体关联场景,关系型数据库配合JPA完全可以稳定支撑,出现的问题只是实体类编写时的基础疏漏,和SQL/NoSQL的选型无关。盲目替换为NoSQL反而需要手动处理大量关联逻辑,增加不必要的开发成本。
内容的提问来源于stack exchange,提问作者James Cervantes
相关产品推荐
相关产品推荐

