什么是逻辑外键?我们要怎么实现逻辑外键?
什么是逻辑外键?
逻辑外键(Logical Foreign Key)是一种 不依赖数据库约束 ,仅通过业务逻辑和字段语义来维护表之间关联关系的设计方式。它本质上是通过在表中定义一个具有特定含义的字段(如
user_id
)来表示与另一张表的关联(如关联user
表的id
,这个也叫user_id
也行,见名知意嘛),但数据库层面不设置FOREIGN KEY
约束。
逻辑外键的核心特征
-
仅通过字段语义关联
用字段名称(如order_id
、dept_id
)表示关联关系,例如order
表的user_id
字段 “语义上” 对应user
表的id
,但数据库不会校验这种关联的有效性。 -
无数据库强制约束
数据库不设置FOREIGN KEY
约束,因此:- 允许插入不存在的关联值(如
user_id=999
但user
表中无此id
); - 删除被关联表的记录时(如删除
user
表的某条数据),数据库不会阻止,需手动处理关联表数据。
- 允许插入不存在的关联值(如
-
依赖应用程序维护一致性
关联关系的有效性(如user_id
必须存在于user
表)完全由代码逻辑保证(如创建订单前校验用户是否存在)。
与物理外键的对比
特性 | 物理外键(Physical Foreign Key) | 逻辑外键(Logical Foreign Key) |
---|---|---|
数据库约束 | 通过FOREIGN KEY 强制关联,不允许无效值 | 无约束,仅通过字段语义关联 |
一致性保障 | 数据库自动校验 | 完全依赖应用代码校验 |
性能影响 | 写入/删除时需校验约束,有性能损耗 | 无额外校验,性能更优 |
灵活性 | 表结构耦合度高,修改困难 | 表结构独立,便于分库分表、结构调整 |
适用场景 | 数据一致性要求极高,低并发场景 | 高并发、分布式系统、快速迭代业务 |
逻辑外键的概念并非源自某一特定的官方标准或学术定义,而是在软件工程实践中,为解决数据库设计与业务需求的矛盾而逐渐形成的经验性设计模式。其核心思想是“用业务逻辑而非数据库约束来维护表之间的关联关系”,这一概念的产生与数据库设计范式、实际业务场景的冲突密切相关。
逻辑外键概念的起源背景(我搜的哈,不一定准)
-
数据库范式与实际需求的矛盾
传统关系型数据库强调通过物理外键(FOREIGN KEY约束)维护表之间的参照完整性,这符合数据库设计的第三范式(3NF),目的是避免数据冗余和不一致。但在实际业务中,物理外键可能带来副作用:- 性能损耗:外键约束会增加数据库写入、更新、删除时的校验开销,在高并发场景下影响效率。
- 灵活性限制:外键约束会强耦合表结构,导致表结构修改(如分库分表、历史数据迁移)变得困难。
- 跨库关联限制:物理外键无法跨数据库实例生效,而分布式系统中表往往分散在不同库。
为了平衡“关联关系维护”与“系统灵活性、性能”,开发者开始采用“仅在表中保留关联字段(如
user_id
),但不创建物理外键约束,通过应用代码逻辑保证参照完整性”的方式,这就是逻辑外键的雏形。 -
面向业务的设计思路普及
随着互联网业务的发展,系统更强调“快速迭代”和“横向扩展”,数据库设计逐渐从“严格遵循范式”转向“以业务需求为中心”。逻辑外键的出现,本质是将“关联关系的维护责任”从数据库转移到应用层,允许开发者根据业务场景灵活控制关联规则(如允许临时的“无效关联”用于特殊业务流程,事后通过补偿机制修复)。 -
ORM框架的推动
MyBatis、Hibernate等ORM框架的普及,进一步强化了逻辑外键的实践。这些框架允许通过代码定义实体间的关联关系(如@ManyToOne
注解、XML中的<association>
标签),而无需依赖数据库的物理外键,使得逻辑外键的实现更加便捷。
怎么应用逻辑外键(代码怎么写)
项目结构
咱们这里以springboot
项目为例,项目结构如下
com.example.demo
├── controller
│ └── OrderController.java // 订单控制器
├── service
│ ├── UserService.java // 用户服务接口
│ ├── OrderService.java // 订单服务接口
│ └── impl
│ ├── UserServiceImpl.java // 用户服务实现
│ └── OrderServiceImpl.java // 订单服务实现
├── mapper
│ ├── UserMapper.java // 用户数据访问接口
│ └── OrderMapper.java // 订单数据访问接口
└── entity├── User.java // 用户实体类└── Order.java // 订单实体类
以下是用户表(user
)和订单表(order
)的可视化展示,清晰体现逻辑外键的关联关系:
假设数据库表如下
用户表(user
)
字段名 | 类型 | 约束 | 说明 |
---|---|---|---|
id | bigint | 主键、自增 | 用户唯一ID |
username | varchar(50) | 非空 | 用户名 |
示例数据:
id | username |
---|---|
1 | Alice |
2 | Bob |
3 | Charlie |
订单表(order
)
字段名 | 类型 | 约束 | 说明 |
---|---|---|---|
id | bigint | 主键、自增 | 订单唯一ID |
order_no | varchar(32) | 非空 | 订单编号(如 ORDER_20231001 ) |
user_id | bigint | 非空 | 逻辑外键,关联 user.id |
示例数据:
id | order_no | user_id(逻辑外键) | 关联的用户(语义上) |
---|---|---|---|
101 | ORDER_20231001 | 1 | Alice(user.id=1) |
102 | ORDER_20231002 | 1 | Alice(user.id=1) |
103 | ORDER_20231003 | 2 | Bob(user.id=2) |
代码示例
在下面的代码中,逻辑外键 通过业务逻辑校验和关联查询代码实现了,而非数据库层面的物理外键约束。
1、验证用户存在性
在创建订单或查询用户订单前,通过userService.existsById(userId)
检查用户ID是否有效。若用户不存在,抛出IllegalArgumentException
中断操作。
2、关联数据查询
在getOrderWithUser
方法中,先查询订单数据,再通过订单中的userId
字段调用userService.getById()
获取关联的用户信息,手动建立对象间关联关系。
package com.example.demo.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.entity.Order;
import com.example.demo.entity.User;
import com.example.demo.mapper.OrderMapper;
import com.example.demo.service.OrderService;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {@Autowiredprivate UserService userService;@Autowiredprivate OrderMapper orderMapper;@Override@Transactionalpublic Order createOrder(Order order) {// 1. 验证逻辑外键:检查用户是否存在Long userId = order.getUserId();if (userId == null || !userService.existsById(userId)) {throw new IllegalArgumentException("无效的用户ID,用户不存在: " + userId);}// 2. 设置订单默认信息order.setOrderNo(generateOrderNo());order.setStatus("PENDING"); // 订单状态:待支付order.setCreateTime(LocalDateTime.now());// 3. 保存订单baseMapper.insert(order);return order;}@Overridepublic List<Order> getOrdersByUserId(Long userId) {// 1. 验证逻辑外键:检查用户是否存在if (userId == null || !userService.existsById(userId)) {throw new IllegalArgumentException("无效的用户ID,用户不存在: " + userId);}// 2. 查询该用户的所有订单return orderMapper.selectByUserId(userId);}@Overridepublic Order getOrderWithUser(Long orderId) {// 1. 查询订单信息Order order = baseMapper.selectById(orderId);if (order == null) {return null;}// 2. 通过逻辑外键查询关联的用户信息User user = userService.getById(order.getUserId());order.setUser(user);return order;}// 生成唯一订单号private String generateOrderNo() {return "ORD" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 8).toUpperCase();}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.OrderMapper"><!-- 基础结果集映射 --><resultMap id="BaseResultMap" type="com.example.demo.entity.Order"><id column="id" property="id"/><result column="order_no" property="orderNo"/><result column="user_id" property="userId"/><result column="amount" property="amount"/><result column="create_time" property="createTime"/></resultMap><!-- 包含用户信息的结果集映射 --><resultMap id="OrderWithUserResultMap" type="com.example.demo.entity.Order" extends="BaseResultMap"><!-- 关联用户信息,property对应Order实体中的user属性 --><association property="user" javaType="com.example.demo.entity.User"><id column="u_id" property="id"/><result column="u_username" property="username"/><result column="u_create_time" property="createTime"/></association></resultMap><!-- 根据ID查询订单 --><select id="selectById" parameterType="java.lang.Long" resultMap="BaseResultMap">SELECT id, order_no, user_id, amount, create_timeFROM `order`WHERE id = #{id}</select><!-- 根据用户ID查询订单 --><select id="selectByUserId" parameterType="java.lang.Long" resultMap="BaseResultMap">SELECT id, order_no, user_id, amount, create_timeFROM `order`WHERE user_id = #{userId}ORDER BY create_time DESC</select><!-- 查询订单及关联的用户信息 --><select id="selectByIdWithUser" parameterType="java.lang.Long" resultMap="OrderWithUserResultMap">SELECT o.id, o.order_no, o.user_id, o.amount, o.create_time,u.id as u_id, u.username as u_username, u.create_time as u_create_timeFROM `order` oLEFT JOIN user u ON o.user_id = u.idWHERE o.id = #{id}</select><!-- 插入订单 --><insert id="insert" parameterType="com.example.demo.entity.Order" useGeneratedKeys="true" keyProperty="id">INSERT INTO `order` (order_no, user_id, amount, create_time)VALUES (#{orderNo,jdbcType=VARCHAR}, #{userId}, #{amount}, #{createTime})</insert></mapper>
想要在本地看一看的,可以下载这个网盘里的代码
我用夸克网盘给你分享了「逻辑外键」,链接:https://pan.quark.cn/s/bef577b5289a