You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 13:36:17