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

JPA复合主键关联异常:Token表插入时user_id为空报错

解决JPA复合主键关联实体时的插入报错问题

你遇到的问题核心是复合主键的映射方式错误——直接把关联的User实体标记为@Id并不是JPA处理复合主键的正确姿势,这会导致JPA无法正确解析主键字段与关联实体的映射关系,最终插入时出现user_id为空、token_id默认值为0的情况,触发数据库非空约束。

问题根源

你的token表是复合主键(token_id, user_id),并且与user表是一对多关联。当你在Token实体中把user字段标记为@Id时,JPA无法将User实体直接映射到数据库的user_id主键字段——JPA要求主键必须是基本数据类型、包装类,或者实现Serializable的复合主键类,而不是关联实体。这种错误的映射会导致:

  1. JPA无法从User实体中提取user_id值填充到数据库字段,导致user_id为空;
  2. token_id因为没有配置正确的生成策略,默认初始化为0,若数据库对token_id有非空/自增约束,也会触发错误。

正确解决方案

针对复合主键+关联实体的场景,JPA提供了两种标准解决方案:@EmbeddedId(嵌入式主键)和**@IdClass**(主键类),下面分别给出具体实现:


方案1:使用@EmbeddedId(推荐,更简洁)

这种方式通过一个嵌入式类来封装复合主键的所有字段,同时通过@MapsId关联到User实体。

步骤1:创建嵌入式主键类TokenId

这个类必须实现Serializable,并重写equals和hashCode方法:

import java.io.Serializable;
import java.util.Objects;
import jakarta.persistence.Embeddable;

@Embeddable
public class TokenId implements Serializable {
    // 对应token表的token_id字段
    private Long tokenId;
    // 对应token表的user_id字段
    private Long userId;

    // 必须提供无参构造器(JPA要求)
    public TokenId() {}

    // 方便创建实例的有参构造器
    public TokenId(Long tokenId, Long userId) {
        this.tokenId = tokenId;
        this.userId = userId;
    }

    // Getter和Setter方法
    public Long getTokenId() { return tokenId; }
    public void setTokenId(Long tokenId) { this.tokenId = tokenId; }
    public Long getUserId() { return userId; }
    public void setUserId(Long userId) { this.userId = userId; }

    // 重写equals和hashCode,用于JPA判断实体唯一性
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        TokenId tokenId1 = (TokenId) o;
        return Objects.equals(tokenId, tokenId1.tokenId) && Objects.equals(userId, tokenId1.userId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(tokenId, userId);
    }
}
步骤2:修改Token实体类

使用@EmbeddedId标记主键,并用@MapsId关联User实体,让JPA自动将User的user_id映射到TokenId的userId字段:

import jakarta.persistence.*;

@Entity
@Table(name = "token")
public class Token {
    @EmbeddedId
    private TokenId id;

    // 关联User实体,@MapsId("userId")表示将User的主键映射到TokenId的userId字段
    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("userId")
    @JoinColumn(name = "user_id")
    private User user;

    // 其他业务字段,比如token值、过期时间等
    @Column(name = "token_value")
    private String tokenValue;

    @Column(name = "expire_time")
    private Long expireTime;

    // 构造器:传入User和业务字段,自动初始化主键的userId部分
    public Token(User user, String tokenValue, Long expireTime) {
        this.id = new TokenId(null, user.getUserId()); // tokenId如果是自增,这里传null
        this.user = user;
        this.tokenValue = tokenValue;
        this.expireTime = expireTime;
    }

    // 无参构造器(JPA要求)
    public Token() {}

    // Getter和Setter方法
    // ...
}
配置token_id自增(如果需要)

如果你的token_id是数据库自增字段,需要在TokenId的tokenId字段上添加@GeneratedValue和@Column注解:

@Embeddable
public class TokenId implements Serializable {
    @GeneratedValue(strategy = GenerationType.IDENTITY) // MySQL自增用IDENTITY
    @Column(name = "token_id")
    private Long tokenId;

    // 其他代码...
}

方案2:使用@IdClass(拆分主键字段)

这种方式将复合主键的每个字段单独标记为@Id,同时通过主键类来封装这些字段。

步骤1:创建主键类TokenId

同样需要实现Serializable并重写equals和hashCode:

import java.io.Serializable;
import java.util.Objects;

public class TokenId implements Serializable {
    private Long tokenId;
    private Long userId;

    public TokenId() {}

    public TokenId(Long tokenId, Long userId) {
        this.tokenId = tokenId;
        this.userId = userId;
    }

    // Getter和Setter方法
    // ...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        TokenId tokenId1 = (TokenId) o;
        return Objects.equals(tokenId, tokenId1.tokenId) && Objects.equals(userId, tokenId1.userId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(tokenId, userId);
    }
}
步骤2:修改Token实体类

用@IdClass指定主键类,分别标记tokenId和userId为@Id,并关联User实体:

import jakarta.persistence.*;

@Entity
@Table(name = "token")
@IdClass(TokenId.class)
public class Token {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 自增配置
    @Column(name = "token_id")
    private Long tokenId;

    @Id
    @Column(name = "user_id")
    private Long userId;

    // 关联User实体,注意insertable=false, updatable=false,避免JPA重复操作userId字段
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", insertable = false, updatable = false)
    private User user;

    // 其他业务字段
    @Column(name = "token_value")
    private String tokenValue;

    // 构造器:传入User和业务字段
    public Token(User user, String tokenValue) {
        this.userId = user.getUserId();
        this.user = user;
        this.tokenValue = tokenValue;
    }

    public Token() {}

    // Getter和Setter方法
    // ...
}

关键注意事项

  1. 复合主键类必须实现Serializable:JPA要求主键类可序列化,用于缓存和持久化操作。
  2. 重写equals和hashCode:确保JPA能正确判断实体的唯一性,避免缓存或查询错误。
  3. 关联实体的映射配置:使用@MapsId(EmbeddedId方案)或insertable=false, updatable=false(IdClass方案),避免JPA对主键字段进行重复操作。
  4. 自增字段的生成策略:根据数据库类型选择合适的生成策略,比如MySQL用GenerationType.IDENTITY,Oracle用GenerationType.SEQUENCE。

验证效果

修改完成后,插入Token数据时,JPA会正确从User实体中提取user_id填充到复合主键,同时token_id会根据配置的生成策略自动生成,不会再出现user_id为空或token_id为0的情况,也就不会触发SQL 1048非空约束错误了。

内容的提问来源于stack exchange,提问作者Jamie Stivala

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 09:40:46