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

我的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)

需要拆分出三张表,明确区分实体本身与实体间的关联属性:

  1. 书籍主表:存储书籍固有属性
  2. 用户表:存储用户固有属性
  3. 用户-书籍价格关联表:存储用户与书籍的绑定关系及专属价格

调整后的实体代码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 07:10:00