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

Spring Boot+JPA+PostgreSQL预订应用User实体设计及关联问题

问题描述

我正在开发基于Spring Boot、JPA和PostgreSQL的预订应用,支持向相关方发送预订提醒,包含Admin、Customer、Employee三种用户类型。

我尝试通过User与各用户类型的@OneToOne关联实现主键共享,实体代码如下:

User实体

@Entity
@Table(name = "userTbl", uniqueConstraints = {@UniqueConstraint(columnNames = "emailAddress")})
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long IID;
    
    @NotEmpty
    private String firstName;

    @NotEmpty
    private String lastName;
    // 其他通用字段...

    @OneToOne(mappedBy = "user")
    @JsonBackReference
    private Admin admin;

    @OneToOne()
    @JsonBackReference
    private Customer customer;

    @OneToOne(mappedBy = "user")
    @JsonBackReference
    private Employee employee;
}

Customer实体(Admin/Employee结构一致)

@Entity
@Table(name = "customer")
public class Customer{
    @Id
    @Column(name = "USER_ID")
    private Long IID;

    @OneToOne()
    @MapsId
    private User user;
    private String paymentID;
    private String invoice;
}

注册报错

注册新用户时出现错误:attempted to assign id from null one-to-one property [com.application.model.Customer.user],注册服务代码如下:

@Service
public class RegisterAuthenticationService {
    private final AuthenticationManager authenticationManager;
    private final UserRepository userRepository;
    private final CustomerRepository customerRepository;
    private final PasswordEncoder passwordEncoder;
    private final JWTService jwtService;

    public RegisterAuthenticationService(AuthenticationManager authenticationManager,
                                         UserRepository userRepository,
                                         CustomerRepository customerRepository,
                                         PasswordEncoder passwordEncoder,
                                         JWTService jwtService) {
        this.authenticationManager = authenticationManager;
        this.userRepository = userRepository;
        this.customerRepository = customerRepository;
        this.passwordEncoder = passwordEncoder;
        this.jwtService = jwtService;
    }

    @Transactional
    public ResponseEntity<?> registerNewCustomer(UserRegisterRequest registerRequest) {
        if (userRepository.existsByEmailAddress(registerRequest.getEmail())) {
            return ResponseEntity.badRequest().body("Email used");
        }

        String type = registerRequest.getType();

        if (type == null
                || !(type.toUpperCase(Locale.ROOT).equals("ADMIN")
                || type.toUpperCase(Locale.ROOT).equals("CUSTOMER")
                || type.toUpperCase(Locale.ROOT).equals("EMPLOYEE"))) {
            throw new RuntimeException("Invalid role");
        }
        
        User user = new User(null, registerRequest.getFirstName(),
                registerRequest.getLastName(),
                registerRequest.getEmail(),
                registerRequest.getPhoneNumber(),
                UserStatus.PENDING,
                UserType.valueOf(registerRequest.getType()),
                registerRequest.getGeographicAddress(),
                new Date().toString(),
                null,
                passwordEncoder.encode(registerRequest.getPassword()));
        userRepository.save(user);
        
        // 重新查询获取ID
        Long iid = userRepository.getUserByEmailAddress(registerRequest.getEmail()).getIID();

        switch (registerRequest.getType().toUpperCase(Locale.ROOT)) {
            case "CUSTOMER" -> {
                Customer userCustomer = new Customer(iid);
                customerRepository.save(userCustomer);
            }
            case "EMPLOYEE" -> {
                // 保存Employee逻辑
            }
            default -> {
                // 保存Admin逻辑
            }
        }

        return ResponseEntity.ok("User registered successfully");
    }
}

其他关联需求

还需设置:

  • Employee与Service的@ManyToMany关联
  • Booking与Customer/Employee的@ManyToOne关联,Booking实体代码如下:
@Entity
@Table(name = "booking")
public class Booking {
    @Id
    @GeneratedValue
    private Long IID;

    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;

    @ManyToOne
    @JoinColumn(name = "employee_id")
    private Employee employee;

    @ManyToOne
    @JoinColumn(name = "service_id")
    private Service service;
}

我也考虑过单表User方案:添加type字段和userCharacteristics存储专属属性,但不确定如何定义后续关联。请问如何设计User实体以支持三种用户类型及相关实体关联?


解决方案

一、先解决当前注册报错问题

报错原因是:@MapsId注解要求Customer的user属性必须非空,因为它要通过关联的User对象获取主键值。你当前创建Customer时只传入了IID,没有设置user属性,导致JPA无法获取主键来源。

修改注册服务代码

  1. 不需要重新查询User:userRepository.save(user)会返回持久化后的User实例,已经包含生成的IID,直接使用即可。
  2. 创建Customer时必须设置关联的User对象:
@Transactional
public ResponseEntity<?> registerNewCustomer(UserRegisterRequest registerRequest) {
    // ... 省略前面的校验逻辑
    
    User user = new User(null, registerRequest.getFirstName(),
            registerRequest.getLastName(),
            registerRequest.getEmail(),
            registerRequest.getPhoneNumber(),
            UserStatus.PENDING,
            UserType.valueOf(registerRequest.getType()),
            registerRequest.getGeographicAddress(),
            new Date().toString(),
            null,
            passwordEncoder.encode(registerRequest.getPassword()));
    
    // 保存User,获取持久化实例
    User savedUser = userRepository.save(user);

    switch (registerRequest.getType().toUpperCase(Locale.ROOT)) {
        case "CUSTOMER" -> {
            Customer customer = new Customer();
            customer.setUser(savedUser); // 设置关联的User对象
            // 其他Customer属性赋值
            customerRepository.save(customer);
        }
        case "EMPLOYEE" -> {
            Employee employee = new Employee();
            employee.setUser(savedUser);
            employeeRepository.save(employee);
        }
        case "ADMIN" -> {
            Admin admin = new Admin();
            admin.setUser(savedUser);
            adminRepository.save(admin);
        }
    }

    return ResponseEntity.ok("User registered successfully");
}

优化实体映射关系

当前User实体中Customer的@OneToOne没有设置mappedBy,会导致JPA认为这是单向关联,可能产生额外的外键列。需要修正为双向关联:

// User实体中的Customer关联
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
@JsonBackReference
private Customer customer;

添加cascade = CascadeType.ALL后,保存User时会自动级联保存关联的Customer,进一步简化代码:

// 注册时可以直接设置User的customer属性,然后只保存User
User savedUser = userRepository.save(user);
Customer customer = new Customer();
customer.setUser(savedUser);
savedUser.setCustomer(customer);
userRepository.save(savedUser);

二、两种用户模型设计方案对比

方案1:一对一共享主键(当前方案)

优点

  • 各用户类型的属性隔离在单独表中,数据结构清晰,符合数据库范式。
  • 扩展新用户类型时,只需新增实体和表,不影响原有表结构。

关联关系设置

  • Employee与Service多对多:在Employee和Service中分别添加双向关联:
// Employee实体
@ManyToMany
@JoinTable(
    name = "employee_service",
    joinColumns = @JoinColumn(name = "employee_id"),
    inverseJoinColumns = @JoinColumn(name = "service_id")
)
private List<Service> services;

// Service实体
@ManyToMany(mappedBy = "services")
private List<Employee> employees;
  • Booking与Customer/Employee多对一:你当前的Booking实体代码已经正确,JPA会自动处理外键关联到对应的用户类型表。

方案2:JPA单表继承(单表User方案)

如果不想用多表关联,可以使用JPA的单表继承策略,将所有用户类型的数据存储在同一张表中。

实现代码

@Entity
@Table(name = "userTbl")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "user_type", discriminatorType = DiscriminatorType.STRING)
public abstract class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long IID;
    
    @NotEmpty
    private String firstName;
    @NotEmpty
    private String lastName;
    // 其他通用字段...
}

@Entity
@DiscriminatorValue("CUSTOMER")
public class Customer extends User {
    private String paymentID;
    private String invoice;
}

@Entity
@DiscriminatorValue("ADMIN")
public class Admin extends User {
    // Admin专属属性
}

@Entity
@DiscriminatorValue("EMPLOYEE")
public class Employee extends User {
    // Employee专属属性
    @ManyToMany
    @JoinTable(
        name = "employee_service",
        joinColumns = @JoinColumn(name = "employee_id"),
        inverseJoinColumns = @JoinColumn(name = "service_id")
    )
    private List<Service> services;
}

优点

  • 查询用户时无需关联多表,性能更高。
  • 注册逻辑更简单,直接保存对应的子类实例即可,无需额外处理关联。

关联关系设置

  • Booking与Customer/Employee多对一:可以直接保留当前的Booking代码,JPA会自动将外键关联到userTbl表的IID列,并通过鉴别器列区分用户类型。
  • 如果你需要在Booking中限制只能关联Customer或Employee,当前代码已经满足需求。

三、方案选择建议

  • 如果各用户类型的专属属性较多,且未来可能频繁扩展新属性,优先选择一对一共享主键方案,避免单表出现大量空字段。
  • 如果用户类型的专属属性较少,且查询性能优先级更高,选择单表继承方案更合适。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 10:07:02