MySQL主键与外键设计原则 + 实战案例解析
作者:Jinkxs
MySQL - 一文搞懂主键与外键:设计原则 + 实战案例
在关系型数据库的设计中,主键(Primary Key)和外键(Foreign Key)是两个基石般的核心概念。它们不仅是数据完整性的保障,更是实现数据关联、维护数据一致性的关键。无论是初学者还是经验丰富的开发者,深入理解主键与外键的工作原理、设计原则以及实际应用场景,对于构建高效、可靠的数据库系统至关重要。本文将带你从零开始,全面解析主键与外键,结合丰富的实战案例和 Java 代码示例,让你彻底掌握这一核心技能。
一、什么是主键(Primary Key)?
1.1 基本定义
主键(Primary Key)是数据库表中用来唯一标识每一行记录的列或列的组合。它是表中最重要的约束之一,确保了表内数据的唯一性和不可重复性。想象一下,你有一个学生名单,每个学生的学号就是主键,它独一无二,不允许重复,也不能为 NULL。
1.2 主键的核心特性
- 唯一性 (Uniqueness): 主键的值在表中必须是唯一的,不能出现重复。
- 非空性 (Not Null): 主键列的值不能为空(NULL)。这确保了每一条记录都有一个明确的身份标识。
- 不可变性 (Immutability): 一旦主键值被设定,通常不应更改。这是为了保证数据引用的稳定性。
- 唯一标识 (Unique Identifier): 主键是表中每一行的唯一标识符。
1.3 主键的类型
- 单列主键 (Single Column Primary Key): 由表中的一个单独列构成主键。这是最常见的形式。
- 示例:
user_id列作为主键。
- 示例:
- 复合主键 (Composite Primary Key): 由表中的多个列组合构成主键。这些列的组合必须唯一。
- 示例:
order_id和product_id组成的复合主键,表示某订单中特定产品的记录。
- 示例:
1.4 主键的创建方式
在 MySQL 中,可以通过以下几种方式定义主键:
在创建表时定义:
CREATE TABLE users (
user_id INT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) UNIQUE
);
在定义列之后定义:
CREATE TABLE orders (
order_id INT,
user_id INT,
order_date DATE,
PRIMARY KEY (order_id) -- 定义主键
);
使用复合主键:
CREATE TABLE order_items (
order_id INT,
product_id INT,
quantity INT,
PRIMARY KEY (order_id, product_id) -- 复合主键
);
1.5 自增主键 (Auto-Increment Primary Key)
最常用的主键类型是自增主键。通过 AUTO_INCREMENT 关键字,MySQL 会自动为新插入的记录分配一个唯一的递增值。
CREATE TABLE products (
product_id INT AUTO_INCREMENT PRIMARY KEY,
product_name VARCHAR(100) NOT NULL,
price DECIMAL(10, 2)
);
二、什么是外键(Foreign Key)?
2.1 基本定义
外键(Foreign Key)是表中的一列或列的组合,它引用另一个表的主键。外键的作用是建立和加强两个表数据之间的链接关系,确保数据的引用完整性。外键的存在使得我们可以轻松地通过一个表关联到另一个表,从而实现数据的关联查询。
2.2 外键的核心特性
- 引用完整性 (Referential Integrity): 外键值必须是被引用表(父表)主键中存在的值,或者为 NULL(如果允许)。
- 级联操作 (Cascade Operations): 可以定义当父表中的记录被修改或删除时,子表中的相关记录应该如何处理(如级联删除、级联更新等)。
- 关联关系 (Relationship): 外键定义了两个表之间的关系,通常是“一对多”或“一对一”的关系。
2.3 外键的关系类型
- 一对多 (One-to-Many): 这是最常见的关系。一个父表的记录可以对应多个子表的记录。例如,一个用户可以有多个订单。
- 一对一 (One-to-One): 一个父表的记录只能对应一个子表的记录。例如,一个用户可能有一个对应的详细信息表。
- 多对多 (Many-to-Many): 通常通过中间表(关联表)来实现。例如,一个学生可以选修多门课程,一门课程也可以被多个学生选修。
2.4 外键的创建方式
在 MySQL 中,外键约束需要在创建表时或之后通过 ADD CONSTRAINT 语句添加。
-- 创建父表
CREATE TABLE categories (
category_id INT PRIMARY KEY,
category_name VARCHAR(100) NOT NULL
);
-- 创建子表并定义外键
CREATE TABLE products (
product_id INT AUTO_INCREMENT PRIMARY KEY,
product_name VARCHAR(100) NOT NULL,
category_id INT,
FOREIGN KEY (category_id) REFERENCES categories(category_id)
);或者,先创建表,再添加外键约束:
CREATE TABLE products (
product_id INT AUTO_INCREMENT PRIMARY KEY,
product_name VARCHAR(100) NOT NULL,
category_id INT
);
ALTER TABLE products
ADD CONSTRAINT fk_category
FOREIGN KEY (category_id) REFERENCES categories(category_id);2.5 外键的级联操作
在定义外键时,可以指定级联操作,以控制当父表记录发生变化时,子表记录的行为。
- CASCADE: 当父表记录被更新或删除时,相关的子表记录也会被自动更新或删除。
- SET NULL: 当父表记录被删除时,子表中对应的外键字段会被设置为 NULL(前提是该字段允许为 NULL)。
- RESTRICT / NO ACTION: 拒绝执行会导致违反外键约束的操作(默认行为)。
- SET DEFAULT: 设置外键字段为默认值(MySQL 5.7+ 支持)。
-- 定义带有级联删除的外键
CREATE TABLE orders (
order_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
order_date DATE,
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
);
三、主键与外键的核心区别
| 特性 | 主键 (Primary Key) | 外键 (Foreign Key) |
|---|---|---|
| 作用 | 唯一标识表中的每一行记录 | 建立表之间的关联关系 |
| 唯一性 | 值必须唯一 | 值可以重复(引用父表的主键值) |
| 非空性 | 必须非空 | 可以为 NULL(除非定义为 NOT NULL) |
| 数量 | 每张表只能有一个主键 | 每张表可以有多个外键 |
| 来源 | 通常由自身表定义 | 引用其他表的主键 |
| 索引 | 自动创建唯一索引 | 通常创建非唯一索引(除非是唯一外键) |
| 数据一致性 | 确保表内数据唯一 | 确保表间数据引用一致 |
四、主键与外键的设计原则
4.1 主键设计原则
- 选择合适的列:
- 自然键: 如果存在天然的唯一标识符(如身份证号、学号),可以考虑使用。
- 代理键: 更推荐使用自增主键或 UUID。自增主键简单、高效,UUID 更适合分布式环境。
- 避免使用业务逻辑字段: 如用户名、邮箱等,因为它们可能变更或重复。
- 保证唯一性:
- 主键值必须在整个表中唯一,这是其核心属性。
- 保证非空性:
- 主键列必须设置为
NOT NULL。
- 主键列必须设置为
- 保持不变性:
- 一旦主键值确定,应尽量避免修改,以维持数据引用的稳定性。
- 考虑性能:
- 尽量选择较小的数据类型(如
INT而不是VARCHAR)以提高索引效率。 - 对于复合主键,将最常用于查询的列放在前面。
- 尽量选择较小的数据类型(如
4.2 外键设计原则
- 明确关联关系:
- 清楚地定义父表和子表之间的关系类型(一对多、一对一等)。
- 选择合适的列:
- 外键列的数据类型必须与被引用的主键列的数据类型完全一致。
- 外键列的长度也必须匹配(如果适用)。
- 考虑约束类型:
- 根据业务需求决定是否启用外键约束,以及是否需要级联操作。
- 外键约束虽然保证了数据一致性,但也可能影响插入和更新的性能。
- 维护数据完整性:
- 外键确保了引用完整性,防止出现孤立记录(即子表中有指向不存在的父表记录)。
- 性能考量:
- 外键会创建索引(通常是非唯一索引),这有助于加速关联查询,但也增加了插入和更新的成本。
- 如果不需要强制引用完整性,可以考虑不使用外键,而通过应用层逻辑来保证。
4.3 设计时的注意事项
- 避免循环引用: 确保表之间的外键关系不会形成循环依赖,这会导致数据库设计混乱和维护困难。
- 合理使用复合主键: 虽然复合主键很强大,但过于复杂的复合主键可能降低查询效率。
- 考虑未来扩展性: 设计时要预留一定的灵活性,以便未来业务变化时能够方便地调整表结构。
- 文档化: 详细记录数据库的结构、主键和外键的定义及它们之间的关系,这对于后续的维护至关重要。
五、实战案例:电商系统的数据库设计
让我们通过一个真实的电商系统案例来深入理解主键与外键的应用。
5.1 需求分析
假设我们要设计一个简单的电商系统,主要功能包括:
- 管理用户(User)
- 管理商品类别(Category)
- 管理商品(Product)
- 管理订单(Order)
- 管理订单项(OrderItem)
我们需要确保:
- 每个用户、商品、类别都有唯一标识。
- 商品必须属于某个类别。
- 订单必须关联到一个用户。
- 订单项必须关联到一个订单和一个商品。
5.2 数据库表结构设计
我们将创建以下表:
- users (用户表): 存储用户基本信息。
- categories (类别表): 存储商品类别信息。
- products (商品表): 存储商品信息,关联到类别。
- orders (订单表): 存储订单信息,关联到用户。
- order_items (订单项表): 存储订单中包含的商品项,关联到订单和商品。
5.2.1 创建用户表 (users)
CREATE TABLE users (
user_id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
- user_id: 自增主键,唯一标识每个用户。
- username: 用户名,非空且唯一。
- email: 邮箱,非空且唯一。
- password_hash: 密码哈希值,非空。
- created_at: 创建时间戳,默认为当前时间。
5.2.2 创建类别表 (categories)
CREATE TABLE categories (
category_id INT AUTO_INCREMENT PRIMARY KEY,
category_name VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
- category_id: 自增主键,唯一标识每个类别。
- category_name: 类别名称,非空且唯一。
- description: 类别描述。
- created_at: 创建时间戳。
5.2.3 创建商品表 (products)
CREATE TABLE products (
product_id INT AUTO_INCREMENT PRIMARY KEY,
product_name VARCHAR(200) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
stock_quantity INT DEFAULT 0,
category_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(category_id) ON DELETE SET NULL
);
- product_id: 自增主键,唯一标识每个商品。
- product_name: 商品名称,非空。
- description: 商品描述。
- price: 商品价格,非空。
- stock_quantity: 库存数量,默认为 0。
- category_id: 外键,引用
categories.category_id。 - created_at: 创建时间戳。
- FOREIGN KEY: 定义外键约束,当
categories中的记录被删除时,products中的category_id会被设置为 NULL(如果category_id允许为 NULL)。
5.2.4 创建订单表 (orders)
CREATE TABLE orders (
order_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
total_amount DECIMAL(10, 2) NOT NULL,
status ENUM('pending', 'processing', 'shipped', 'delivered', 'cancelled') DEFAULT 'pending',
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
);
- order_id: 自增主键,唯一标识每个订单。
- user_id: 外键,引用
users.user_id。 - order_date: 订单日期,默认为当前时间。
- total_amount: 订单总金额,非空。
- status: 订单状态,默认为 ‘pending’。
- FOREIGN KEY: 定义外键约束,当
users中的记录被删除时,所有相关的orders记录也会被自动删除(级联删除)。
5.2.5 创建订单项表 (order_items)
CREATE TABLE order_items (
order_item_id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL DEFAULT 1,
unit_price DECIMAL(10, 2) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(product_id) ON DELETE CASCADE
);
- order_item_id: 自增主键,唯一标识每个订单项。
- order_id: 外键,引用
orders.order_id。 - product_id: 外键,引用
products.product_id。 - quantity: 购买数量,默认为 1。
- unit_price: 单价,非空。
- FOREIGN KEY: 定义两个外键约束,分别关联到
orders和products表。当订单或商品被删除时,相关的订单项也会被自动删除。
5.3 表关系图解

5.4 数据插入示例
-- 插入用户数据
INSERT INTO users (username, email, password_hash) VALUES
('alice', 'alice@example.com', 'hashed_password_1'),
('bob', 'bob@example.com', 'hashed_password_2');
-- 插入类别数据
INSERT INTO categories (category_name, description) VALUES
('Electronics', 'Electronic devices and gadgets'),
('Books', 'Books and literature');
-- 插入商品数据
INSERT INTO products (product_name, description, price, stock_quantity, category_id) VALUES
('Smartphone', 'Latest model smartphone', 699.99, 50, 1),
('Laptop', 'High-performance laptop', 1299.99, 20, 1),
('Novel', 'Popular fiction novel', 12.99, 100, 2);
-- 插入订单数据
INSERT INTO orders (user_id, total_amount, status) VALUES
(1, 712.98, 'delivered'), -- Alice's order for 1 Smartphone (699.99) + 1 Novel (12.99)
(2, 1312.98, 'processing'); -- Bob's order for 1 Laptop (1299.99) + 1 Novel (12.99)
-- 插入订单项数据
INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES
(1, 1, 1, 699.99), -- Alice ordered 1 Smartphone
(1, 3, 1, 12.99), -- Alice ordered 1 Novel
(2, 2, 1, 1299.99), -- Bob ordered 1 Laptop
(2, 3, 1, 12.99); -- Bob ordered 1 Novel六、Java 代码示例:Spring Boot + JPA 实现
我们将使用 Spring Boot 和 JPA 来实现上述电商系统的实体类和 Repository,以展示如何在 Java 应用中处理主键与外键。
6.1 项目结构
src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── example/ │ │ └── ecommerce/ │ │ ├── EcommerceApplication.java │ │ ├── config/ │ │ │ └── DatabaseConfig.java (可选) │ │ ├── entity/ │ │ │ ├── User.java │ │ │ ├── Category.java │ │ │ ├── Product.java │ │ │ ├── Order.java │ │ │ └── OrderItem.java │ │ ├── repository/ │ │ │ ├── UserRepository.java │ │ │ ├── CategoryRepository.java │ │ │ ├── ProductRepository.java │ │ │ ├── OrderRepository.java │ │ │ └── OrderItemRepository.java │ │ ├── service/ │ │ │ ├── UserService.java │ │ │ ├── ProductService.java │ │ │ ├── OrderService.java │ │ │ └── OrderItemService.java │ │ └── controller/ │ │ ├── UserController.java │ │ ├── ProductController.java │ │ └── OrderController.java │ └── resources/ │ ├── application.properties │ └── data.sql (可选,用于初始化数据) └── pom.xml
6.2 Maven 依赖 (pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>ecommerce</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ecommerce</name>
<description>E-commerce System Example</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version> <!-- 使用兼容的版本 -->
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 添加 Lombok 依赖以简化实体类代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>6.3 配置文件 (application.properties)
# Database Configuration spring.datasource.url=jdbc:mysql://localhost:3306/ecommerce_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true spring.datasource.username=your_username spring.datasource.password=your_password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # JPA Configuration spring.jpa.hibernate.ddl-auto=update # 仅用于演示,生产环境应谨慎使用 spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect spring.jpa.properties.hibernate.format_sql=true # Lombok configuration (if using) # lombok.mapstruct.default-component-model=spring
6.4 实体类 (Entity)
User.java
package com.example.ecommerce.entity;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long userId; // 主键,自增
@Column(name = "username", nullable = false, unique = true)
private String username; // 用户名
@Column(name = "email", nullable = false, unique = true)
private String email; // 邮箱
@Column(name = "password_hash", nullable = false)
private String passwordHash; // 密码哈希
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt; // 创建时间
// 与 Order 的一对多关系 (一个用户可以有多个订单)
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Order> orders;
// 构造函数
public User() {}
public User(String username, String email, String passwordHash) {
this.username = username;
this.email = email;
this.passwordHash = passwordHash;
this.createdAt = LocalDateTime.now(); // 自动设置创建时间
}
// Getter 和 Setter 方法
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPasswordHash() {
return passwordHash;
}
public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public List<Order> getOrders() {
return orders;
}
public void setOrders(List<Order> orders) {
this.orders = orders;
}
@Override
public String toString() {
return "User{" +
"userId=" + userId +
", username='" + username + '\'' +
", email='" + email + '\'' +
", createdAt=" + createdAt +
'}';
}
}Category.java
package com.example.ecommerce.entity;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "categories")
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "category_id")
private Long categoryId; // 主键,自增
@Column(name = "category_name", nullable = false, unique = true)
private String categoryName; // 类别名称
@Column(name = "description", columnDefinition = "TEXT")
private String description; // 类别描述
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt; // 创建时间
// 与 Product 的一对多关系 (一个类别可以有多个商品)
@OneToMany(mappedBy = "category", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Product> products;
// 构造函数
public Category() {}
public Category(String categoryName, String description) {
this.categoryName = categoryName;
this.description = description;
this.createdAt = LocalDateTime.now(); // 自动设置创建时间
}
// Getter 和 Setter 方法
public Long getCategoryId() {
return categoryId;
}
public void setCategoryId(Long categoryId) {
this.categoryId = categoryId;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public List<Product> getProducts() {
return products;
}
public void setProducts(List<Product> products) {
this.products = products;
}
@Override
public String toString() {
return "Category{" +
"categoryId=" + categoryId +
", categoryName='" + categoryName + '\'' +
", description='" + description + '\'' +
", createdAt=" + createdAt +
'}';
}
}Product.java
package com.example.ecommerce.entity;
import javax.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_id")
private Long productId; // 主键,自增
@Column(name = "product_name", nullable = false)
private String productName; // 商品名称
@Column(name = "description", columnDefinition = "TEXT")
private String description; // 商品描述
@Column(name = "price", nullable = false, precision = 10, scale = 2)
private BigDecimal price; // 商品价格
@Column(name = "stock_quantity", nullable = false, defaultValue = "0")
private Integer stockQuantity; // 库存数量
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt; // 创建时间
// 与 Category 的多对一关系 (一个商品属于一个类别)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id") // 外键,引用 Category.category_id
private Category category;
// 与 OrderItem 的一对多关系 (一个商品可以出现在多个订单项中)
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private java.util.List<OrderItem> orderItems;
// 构造函数
public Product() {}
public Product(String productName, String description, BigDecimal price, Integer stockQuantity, Category category) {
this.productName = productName;
this.description = description;
this.price = price;
this.stockQuantity = stockQuantity;
this.category = category;
this.createdAt = LocalDateTime.now(); // 自动设置创建时间
}
// Getter 和 Setter 方法
public Long getProductId() {
return productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
public Integer getStockQuantity() {
return stockQuantity;
}
public void setStockQuantity(Integer stockQuantity) {
this.stockQuantity = stockQuantity;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
public java.util.List<OrderItem> getOrderItems() {
return orderItems;
}
public void setOrderItems(java.util.List<OrderItem> orderItems) {
this.orderItems = orderItems;
}
@Override
public String toString() {
return "Product{" +
"productId=" + productId +
", productName='" + productName + '\'' +
", description='" + description + '\'' +
", price=" + price +
", stockQuantity=" + stockQuantity +
", createdAt=" + createdAt +
", category=" + (category != null ? category.getCategoryName() : "null") +
'}';
}
}Order.java
package com.example.ecommerce.entity;
import javax.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_id")
private Long orderId; // 主键,自增
@Column(name = "order_date", nullable = false)
private LocalDateTime orderDate; // 订单日期
@Column(name = "total_amount", nullable = false, precision = 10, scale = 2)
private BigDecimal totalAmount; // 订单总金额
@Column(name = "status", nullable = false, columnDefinition = "ENUM('pending', 'processing', 'shipped', 'delivered', 'cancelled')")
private String status; // 订单状态
// 与 User 的多对一关系 (一个订单属于一个用户)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false) // 外键,引用 User.user_id
private User user;
// 与 OrderItem 的一对多关系 (一个订单包含多个订单项)
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<OrderItem> orderItems;
// 构造函数
public Order() {}
public Order(User user, BigDecimal totalAmount, String status) {
this.user = user;
this.totalAmount = totalAmount;
this.status = status;
this.orderDate = LocalDateTime.now(); // 自动设置订单日期
}
// Getter 和 Setter 方法
public Long getOrderId() {
return orderId;
}
public void setOrderId(Long orderId) {
this.orderId = orderId;
}
public LocalDateTime getOrderDate() {
return orderDate;
}
public void setOrderDate(LocalDateTime orderDate) {
this.orderDate = orderDate;
}
public BigDecimal getTotalAmount() {
return totalAmount;
}
public void setTotalAmount(BigDecimal totalAmount) {
this.totalAmount = totalAmount;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public List<OrderItem> getOrderItems() {
return orderItems;
}
public void setOrderItems(List<OrderItem> orderItems) {
this.orderItems = orderItems;
}
@Override
public String toString() {
return "Order{" +
"orderId=" + orderId +
", orderDate=" + orderDate +
", totalAmount=" + totalAmount +
", status='" + status + '\'' +
", user=" + (user != null ? user.getUsername() : "null") +
'}';
}
}OrderItem.java
package com.example.ecommerce.entity;
import javax.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "order_items")
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_item_id")
private Long orderItemId; // 主键,自增
@Column(name = "quantity", nullable = false, defaultValue = "1")
private Integer quantity; // 购买数量
@Column(name = "unit_price", nullable = false, precision = 10, scale = 2)
private BigDecimal unitPrice; // 单价
// 与 Order 的多对一关系 (一个订单项属于一个订单)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false) // 外键,引用 Order.order_id
private Order order;
// 与 Product 的多对一关系 (一个订单项包含一个商品)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", nullable = false) // 外键,引用 Product.product_id
private Product product;
// 构造函数
public OrderItem() {}
public OrderItem(Order order, Product product, Integer quantity, BigDecimal unitPrice) {
this.order = order;
this.product = product;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
// Getter 和 Setter 方法
public Long getOrderItemId() {
return orderItemId;
}
public void setOrderItemId(Long orderItemId) {
this.orderItemId = orderItemId;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public BigDecimal getUnitPrice() {
return unitPrice;
}
public void setUnitPrice(BigDecimal unitPrice) {
this.unitPrice = unitPrice;
}
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
@Override
public String toString() {
return "OrderItem{" +
"orderItemId=" + orderItemId +
", quantity=" + quantity +
", unitPrice=" + unitPrice +
", order=" + (order != null ? order.getOrderId() : "null") +
", product=" + (product != null ? product.getProductName() : "null") +
'}';
}
}6.5 Repository 接口
UserRepository.java
package com.example.ecommerce.repository;
import com.example.ecommerce.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
}CategoryRepository.java
package com.example.ecommerce.repository;
import com.example.ecommerce.entity.Category;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface CategoryRepository extends JpaRepository<Category, Long> {
Optional<Category> findByCategoryName(String categoryName);
}ProductRepository.java
package com.example.ecommerce.repository;
import com.example.ecommerce.entity.Product;
import com.example.ecommerce.entity.Category;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByCategory(Category category);
}OrderRepository.java
package com.example.ecommerce.repository;
import com.example.ecommerce.entity.Order;
import com.example.ecommerce.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByUser(User user);
}OrderItemRepository.java
package com.example.ecommerce.repository;
import com.example.ecommerce.entity.OrderItem;
import com.example.ecommerce.entity.Order;
import com.example.ecommerce.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface OrderItemRepository extends JpaRepository<OrderItem, Long> {
List<OrderItem> findByOrder(Order order);
List<OrderItem> findByProduct(Product product);
}6.6 Service 层 (部分示例)
OrderService.java
package com.example.ecommerce.service;
import com.example.ecommerce.entity.*;
import com.example.ecommerce.repository.OrderItemRepository;
import com.example.ecommerce.repository.OrderRepository;
import com.example.ecommerce.repository.ProductRepository;
import com.example.ecommerce.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Service
@Transactional // 确保事务管理
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private ProductRepository productRepository;
@Autowired
private OrderItemRepository orderItemRepository;
// 创建订单的示例方法
public Order createOrder(Long userId, List<Long> productIdsAndQuantities) {
// 1. 获取用户
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found with ID: " + userId));
// 2. 创建订单对象
Order order = new Order(user, BigDecimal.ZERO, "pending");
order = orderRepository.save(order); // 保存订单以获取自动生成的 orderId
// 3. 计算总价并创建订单项
BigDecimal totalAmount = BigDecimal.ZERO;
List<OrderItem> orderItems = new ArrayList<>();
for (Long[] item : productIdsAndQuantities) {
Long productId = item[0];
Integer quantity = item[1].intValue();
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found with ID: " + productId));
if (product.getStockQuantity() < quantity) {
throw new RuntimeException("Insufficient stock for product: " + product.getProductName());
}
BigDecimal itemTotal = product.getPrice().multiply(BigDecimal.valueOf(quantity));
totalAmount = totalAmount.add(itemTotal);
OrderItem orderItem = new OrderItem(order, product, quantity, product.getPrice());
orderItems.add(orderItem);
}
// 4. 保存订单项
orderItemRepository.saveAll(orderItems);
// 5. 更新订单总价
order.setTotalAmount(totalAmount);
order = orderRepository.save(order);
// 6. (可选) 更新商品库存
// 这里可以添加逻辑来减少商品库存
// 为简单起见,这里省略
return order;
}
// 获取用户的所有订单
public List<Order> getOrdersByUser(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found with ID: " + userId));
return orderRepository.findByUser(user);
}
// 获取订单详情(包括订单项)
public Order getOrderDetails(Long orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("Order not found with ID: " + orderId));
}
}6.7 Controller 层 (部分示例)
OrderController.java
package com.example.ecommerce.controller;
import com.example.ecommerce.entity.Order;
import com.example.ecommerce.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
// 创建订单
@PostMapping("/create")
public ResponseEntity<Order> createOrder(
@RequestParam Long userId,
@RequestBody List<long[]> productIdsAndQuantities) { // 使用 long[] 数组传递 [productId, quantity]
try {
// 注意:这里简化了参数传递。在实际应用中,通常会使用 DTO。
// 例如,定义一个 OrderRequestDTO 包含 userId 和 List<OrderItemRequestDTO>
// 这里直接使用 Long[] 数组模拟 [productId, quantity] 对
List<Long[]> items = new java.util.ArrayList<>();
for (long[] item : productIdsAndQuantities) {
items.add(new Long[]{item[0], item[1]}); // 转换为 Long[]
}
Order order = orderService.createOrder(userId, items);
return ResponseEntity.ok(order);
} catch (Exception e) {
return ResponseEntity.badRequest().build(); // 或者返回具体错误信息
}
}
// 获取用户的所有订单
@GetMapping("/user/{userId}")
public ResponseEntity<List<Order>> getOrdersByUser(@PathVariable Long userId) {
List<Order> orders = orderService.getOrdersByUser(userId);
return ResponseEntity.ok(orders);
}
// 获取订单详情
@GetMapping("/{orderId}")
public ResponseEntity<Order> getOrderDetails(@PathVariable Long orderId) {
Order order = orderService.getOrderDetails(orderId);
return ResponseEntity.ok(order);
}
}6.8 启动类与主程序
EcommerceApplication.java
package com.example.ecommerce;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class EcommerceApplication {
public static void main(String[] args) {
SpringApplication.run(EcommerceApplication.class, args);
}
}6.9 运行与测试
- 配置数据库: 确保你的 MySQL 数据库已运行,并且在
application.properties中正确配置了数据库连接信息。 - 运行项目: 使用 Maven (
mvn spring-boot:run) 或 IDE 启动 Spring Boot 应用。 - 测试 API:
- 创建用户:
curl -X POST http://localhost:8080/api/users \ -H "Content-Type: application/json" \ -d '{"username":"alice","email":"alice@example.com","passwordHash":"hashed_password_1"}' - 创建类别:
curl -X POST http://localhost:8080/api/categories \ -H "Content-Type: application/json" \ -d '{"categoryName":"Electronics","description":"Electronic devices and gadgets"}' - 创建商品:
curl -X POST http://localhost:8080/api/products \ -H "Content-Type: application/json" \ -d '{"productName":"Smartphone","description":"Latest model smartphone","price":699.99,"stockQuantity":50,"category":{"categoryId":1}}' - 创建订单:
curl -X POST http://localhost:8080/api/orders/create?userId=1 \ -H "Content-Type: application/json" \ -d '[ [1, 1] ]' # [productId, quantity] - 获取订单详情:
curl -X GET http://localhost:8080/api/orders/1
- 创建用户:
通过这种方式,JPA 会自动处理主键的自动生成(@GeneratedValue(strategy = GenerationType.IDENTITY)),以及外键的关联(@ManyToOne, @OneToMany 注解)。Java 对象之间的关联关系映射到了数据库中的主键和外键约束上,实现了 ORM(对象关系映射)。
七、主键与外键的性能影响
7.1 索引与查询性能
- 主键索引: 每个表的主键都会自动创建一个唯一索引(Primary Key Index)。这个索引是聚簇索引(Clustered Index)在 InnoDB 存储引擎中,它决定了数据在磁盘上的物理存储顺序。查询主键的性能非常高,因为它直接定位到数据页。
- 外键索引: 外键列通常也会被创建索引(除非显式指定不创建)。这有助于加速 JOIN 查询和外键约束检查。但请注意,外键索引会增加插入和更新操作的开销。
7.2 插入性能
- 主键: 插入新记录时,主键值需要满足唯一性约束。对于自增主键,这个过程非常高效。
- 外键: 插入记录时,数据库需要检查外键约束。如果外键引用的表中有大量的数据,这个检查可能会花费一些时间。此外,如果外键列上有索引,插入操作还需要维护索引。
7.3 更新与删除性能
- 主键: 更新主键(如果允许)通常代价很高,因为它需要重新组织数据以适应新的主键值。因此,一般不建议修改主键。
- 外键: 删除父表中的记录时,如果设置了级联删除(
ON DELETE CASCADE),那么相关的子表记录也会被删除。这可能会影响性能,特别是当子表记录很多时。同样,更新外键值也需要检查约束并可能更新索引。
7.4 内存与缓存
- 索引内存: 主键和外键索引都会占用内存。在内存充足的环境中,这通常不是问题,但需要考虑索引对内存的消耗。
- 缓存效率: 由于主键索引是聚簇索引,它在缓存中通常表现更好。外键索引也可能提升查询缓存的效果。
八、主键与外键的高级话题
8.1 外键约束的管理
- 启用/禁用约束: 在某些情况下,可能需要临时禁用外键约束以进行批量操作。MySQL 提供了
SET FOREIGN_KEY_CHECKS语句来控制。SET FOREIGN_KEY_CHECKS = 0; -- 禁用外键检查 -- 执行批量操作 SET FOREIGN_KEY_CHECKS = 1; -- 启用外键检查
- 检查约束: 可以使用
SHOW CREATE TABLE table_name;查看表的结构,包括主键和外键约束的定义。
8.2 复合主键与复合外键
- 复合主键: 如前面示例所示,由多个列组成的主键。
- 复合外键: 在某些情况下,外键也可能由多个列组成,以确保引用的唯一性。
8.3 外键与事务
- 事务一致性: 外键约束在事务中工作良好,确保了事务内的数据一致性。如果在事务中违反了外键约束,整个事务将回滚。
- 死锁: 在并发环境中,不当的外键操作可能导致死锁。需要合理设计索引和事务隔离级别。
8.4 无主键表 (MyISAM)
- MyISAM 存储引擎: 在 MyISAM 存储引擎中,表可以没有主键。在这种情况下,表中的每一行通过行号(Row Number)来标识。然而,MyISAM 已经被 InnoDB 取代,不推荐在新项目中使用。
8.5 UUID 作为主键
- 优点: UUID 是全球唯一的,非常适合分布式系统,避免了主键冲突的问题。
- 缺点: UUID 是字符串类型,比整数类型占用更多空间,且可能导致索引碎片,影响性能。如果需要使用 UUID,通常会使用
CHAR(36)或BINARY(16)类型。
九、常见陷阱与注意事项
9.1 主键陷阱
- 使用业务字段作为主键: 如果业务字段(如用户名、邮箱)在未来可能会变更,将其用作主键会导致严重问题。
- 复合主键设计不当: 如果复合主键中的列顺序不合理,或者包含经常变动的列,可能会影响查询性能和维护性。
- 不使用自增主键: 虽然 UUID 等代理键是好的选择,但在性能要求极高的场景下,自增主键仍然是首选。
9.2 外键陷阱
- 忘记创建外键索引: 外键列如果没有索引,会导致查询性能急剧下降。虽然外键约束会自动创建索引,但有时可能需要手动优化。
- 级联操作滥用: 过度使用
ON DELETE CASCADE可能导致意外的数据删除。应谨慎使用,确保业务逻辑清晰。 - 循环外键: 设计时应避免表之间的循环引用,这会使数据库结构复杂化,难以维护。
- 外键与存储引擎: 外键约束在 MyISAM 存储引擎中是不支持的,只能在 InnoDB 中使用。确保使用正确的存储引擎。
9.3 数据完整性与业务逻辑
- 外键不是万能的: 外键约束可以保证数据库层面的引用完整性,但不能替代业务逻辑验证。例如,一个订单状态的变更需要符合业务规则,这需要在应用层实现。
- 空值处理: 外键列可以为 NULL(如果允许),但需要明确其含义。通常,NULL 表示“没有关联”或“未指定”。
- 数据一致性: 在应用层处理数据时,务必确保数据的一致性。例如,在删除用户时,确保所有相关的订单也被正确处理。
十、总结与展望
主键与外键是关系型数据库设计的核心支柱。它们不仅确保了数据的唯一性和完整性,还为我们提供了强大的数据关联能力。通过本文的讲解和示例,你应该对主键和外键有了深刻的理解。
- 主键: 是表的唯一标识符,保证了行的唯一性。选择合适的主键类型(自增、UUID 等)对性能和扩展性至关重要。
- 外键: 是表间关联的桥梁,维护了数据的引用完整性。合理设计外键关系,可以简化复杂的查询和数据操作。
- 设计原则: 在设计数据库时,遵循明确的主键和外键设计原则,有助于构建稳定、高效的系统。
- 实践应用: 通过 Spring Boot 和 JPA 的实践,我们看到了如何在 Java 应用中优雅地处理主键和外键关系。
随着数据库技术的发展,新的存储引擎和优化策略不断涌现。但主键和外键的基本原理和设计思想依然不变。掌握这些知识,不仅能帮助你构建更可靠的数据库应用,也为学习更高级的数据库技术和架构打下了坚实的基础。
记住,好的数据库设计是一个持续的过程。随着业务的发展和需求的变化,定期回顾和优化你的数据库结构是非常必要的。希望这篇文章能成为你数据库设计之旅中的一个重要里程碑!
附录:相关资源链接
- MySQL 8.0 官方文档 - Primary Keys
- MySQL 8.0 官方文档 - Foreign Keys
- MySQL 8.0 官方文档 - Creating Tables
- Spring Boot 官方文档 - Using JPA with Spring Boot
- Hibernate 官方文档 - Mapping Basic Types
- W3Schools - SQL Primary Key
- W3Schools - SQL Foreign Key
图表:主键与外键关系图

希望这篇全面的指南能帮助你彻底掌握主键与外键的概念、设计原则和实战应用。记住,实践是最好的老师,多动手练习,你就能在数据库设计的道路上越走越远!
到此这篇关于MySQL - 一文搞懂主键与外键:设计原则 + 实战案例的文章就介绍到这了,更多相关mysql主键与外键内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
