Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > mysql主键与外键

MySQL主键与外键设计原则 + 实战案例解析

作者:Jinkxs

这篇文章详细介绍了MySQL中的主键和外键,包括它们的基本概念、设计原则、应用场景以及实战案例,主键用于唯一标识表中的每一行记录,而外键用于建立和加强表之间的关联关系,感兴趣的朋友跟随小编一起看看吧

MySQL - 一文搞懂主键与外键:设计原则 + 实战案例 

在关系型数据库的设计中,主键(Primary Key)和外键(Foreign Key)是两个基石般的核心概念。它们不仅是数据完整性的保障,更是实现数据关联、维护数据一致性的关键。无论是初学者还是经验丰富的开发者,深入理解主键与外键的工作原理、设计原则以及实际应用场景,对于构建高效、可靠的数据库系统至关重要。本文将带你从零开始,全面解析主键与外键,结合丰富的实战案例和 Java 代码示例,让你彻底掌握这一核心技能。

一、什么是主键(Primary Key)?

1.1 基本定义

主键(Primary Key)是数据库表中用来唯一标识每一行记录的列或列的组合。它是表中最重要的约束之一,确保了表内数据的唯一性和不可重复性。想象一下,你有一个学生名单,每个学生的学号就是主键,它独一无二,不允许重复,也不能为 NULL。

1.2 主键的核心特性

1.3 主键的类型

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 外键的核心特性

2.3 外键的关系类型

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 外键的级联操作

在定义外键时,可以指定级联操作,以控制当父表记录发生变化时,子表记录的行为。

-- 定义带有级联删除的外键
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 主键设计原则

  1. 选择合适的列:
    • 自然键: 如果存在天然的唯一标识符(如身份证号、学号),可以考虑使用。
    • 代理键: 更推荐使用自增主键或 UUID。自增主键简单、高效,UUID 更适合分布式环境。
    • 避免使用业务逻辑字段: 如用户名、邮箱等,因为它们可能变更或重复。
  2. 保证唯一性:
    • 主键值必须在整个表中唯一,这是其核心属性。
  3. 保证非空性:
    • 主键列必须设置为 NOT NULL
  4. 保持不变性:
    • 一旦主键值确定,应尽量避免修改,以维持数据引用的稳定性。
  5. 考虑性能:
    • 尽量选择较小的数据类型(如 INT 而不是 VARCHAR)以提高索引效率。
    • 对于复合主键,将最常用于查询的列放在前面。

4.2 外键设计原则

  1. 明确关联关系:
    • 清楚地定义父表和子表之间的关系类型(一对多、一对一等)。
  2. 选择合适的列:
    • 外键列的数据类型必须与被引用的主键列的数据类型完全一致。
    • 外键列的长度也必须匹配(如果适用)。
  3. 考虑约束类型:
    • 根据业务需求决定是否启用外键约束,以及是否需要级联操作。
    • 外键约束虽然保证了数据一致性,但也可能影响插入和更新的性能。
  4. 维护数据完整性:
    • 外键确保了引用完整性,防止出现孤立记录(即子表中有指向不存在的父表记录)。
  5. 性能考量:
    • 外键会创建索引(通常是非唯一索引),这有助于加速关联查询,但也增加了插入和更新的成本。
    • 如果不需要强制引用完整性,可以考虑不使用外键,而通过应用层逻辑来保证。

4.3 设计时的注意事项

五、实战案例:电商系统的数据库设计

让我们通过一个真实的电商系统案例来深入理解主键与外键的应用。

5.1 需求分析

假设我们要设计一个简单的电商系统,主要功能包括:

我们需要确保:

5.2 数据库表结构设计

我们将创建以下表:

  1. users (用户表): 存储用户基本信息。
  2. categories (类别表): 存储商品类别信息。
  3. products (商品表): 存储商品信息,关联到类别。
  4. orders (订单表): 存储订单信息,关联到用户。
  5. 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
);
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
);
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
);
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
);
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
);

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 运行与测试

  1. 配置数据库: 确保你的 MySQL 数据库已运行,并且在 application.properties 中正确配置了数据库连接信息。
  2. 运行项目: 使用 Maven (mvn spring-boot:run) 或 IDE 启动 Spring Boot 应用。
  3. 测试 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 索引与查询性能

7.2 插入性能

7.3 更新与删除性能

7.4 内存与缓存

八、主键与外键的高级话题

8.1 外键约束的管理

8.2 复合主键与复合外键

8.3 外键与事务

8.4 无主键表 (MyISAM)

8.5 UUID 作为主键

九、常见陷阱与注意事项

9.1 主键陷阱

9.2 外键陷阱

9.3 数据完整性与业务逻辑

十、总结与展望

主键与外键是关系型数据库设计的核心支柱。它们不仅确保了数据的唯一性和完整性,还为我们提供了强大的数据关联能力。通过本文的讲解和示例,你应该对主键和外键有了深刻的理解。

随着数据库技术的发展,新的存储引擎和优化策略不断涌现。但主键和外键的基本原理和设计思想依然不变。掌握这些知识,不仅能帮助你构建更可靠的数据库应用,也为学习更高级的数据库技术和架构打下了坚实的基础。

记住,好的数据库设计是一个持续的过程。随着业务的发展和需求的变化,定期回顾和优化你的数据库结构是非常必要的。希望这篇文章能成为你数据库设计之旅中的一个重要里程碑!

附录:相关资源链接

图表:主键与外键关系图

希望这篇全面的指南能帮助你彻底掌握主键与外键的概念、设计原则和实战应用。记住,实践是最好的老师,多动手练习,你就能在数据库设计的道路上越走越远!

到此这篇关于MySQL - 一文搞懂主键与外键:设计原则 + 实战案例的文章就介绍到这了,更多相关mysql主键与外键内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文