我的Spring Boot实体是否规范化?若不符合该如何调整?
问题分析与解决方案
问题描述
现有两个存在一对多关联的表,实体代码如下:
UserDetails 实体
public class UserDetails { @Id @Column(nullable = false) private String userId; @OneToMany(targetEntity = Books.class, cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) @JoinColumn(name = "userId", referencedColumnName = "userId", updatable = true, insertable = true) private List<Books> books = new ArrayList<>(); }
Books 实体
public class Books { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @JsonIgnore private Long id; @Column(nullable = false) private String bookName; @Column(nullable = false) private Double bookPrice; }
需求场景:同一本书对不同用户可设置不同价格。请问当前实体设计是否符合数据库规范化?若不符合,应如何调整?
解决方案
当前设计不符合数据库规范化
核心问题在于:Books实体混合了两类属性——bookName是书籍的固有属性(同一本书无论关联多少用户,名称不会变),而bookPrice是和用户绑定的专属属性。这种设计会引发两个关键问题:
- 数据冗余:同一本书给N个用户设置价格,就要重复存储N次
bookName; - 更新异常:如果书籍名称需要修改,必须更新所有关联该书籍的用户记录,极易出现数据不一致。
调整方案(符合第三范式3NF)
需要拆分出三张表,明确区分实体本身与实体间的关联属性:
- 书籍主表:存储书籍固有属性
- 用户表:存储用户固有属性
- 用户-书籍价格关联表:存储用户与书籍的绑定关系及专属价格
调整后的实体代码
Book 实体(书籍主表)
public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long bookId; @Column(nullable = false, unique = true) private String bookName; // 可扩展其他书籍固有属性:ISBN、作者、出版社等 }
UserDetails 实体(用户表)
public class UserDetails { @Id @Column(nullable = false) private String userId; // 可扩展用户其他属性:用户名、邮箱等 @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List<UserBookPrice> userBookPrices = new ArrayList<>(); }
UserBookPrice 实体(用户书籍价格关联表)
public class UserBookPrice { @EmbeddedId private UserBookPriceId id; @Column(nullable = false) private Double bookPrice; @ManyToOne @MapsId("userId") @JoinColumn(name = "user_id") private UserDetails user; @ManyToOne @MapsId("bookId") @JoinColumn(name = "book_id") private Book book; // 复合主键类,确保同一用户同一书籍仅存一条价格记录 @Embeddable public static class UserBookPriceId implements Serializable { private String userId; private Long bookId; public UserBookPriceId() {} public UserBookPriceId(String userId, Long bookId) { this.userId = userId; this.bookId = bookId; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserBookPriceId that = (UserBookPriceId) o; return Objects.equals(userId, that.userId) && Objects.equals(bookId, that.bookId); } @Override public int hashCode() { return Objects.hash(userId, bookId); } } }
对应表结构说明
book表:book_id(主键)、book_name(唯一约束)user_details表:user_id(主键)user_book_price表:user_id(外键关联user_details.user_id)、book_id(外键关联book.book_id)、book_price,其中user_id+book_id作为复合主键。
调整后既消除了数据冗余,保证了数据一致性,同时完美支持“同一本书对不同用户设置不同价格”的需求。
内容的提问来源于stack exchange,提问作者shiningStar
相关产品推荐
相关产品推荐

