TL;DR
- 场景:数据库设计中1对1关系的建模,以及使用MyBatis框架实现跨表查询
- 结论:1对1关系通过主键关联或外键+唯一约束实现,适合数据拆分、权限控制、扩展存储等场景;MyBatis通过resultMap和association标签完成实体映射
- 产出:完整的建表SQL、MyBatis映射配置、Java实体类代码,可直接移植到项目中使用

版本矩阵
| 功能 | 版本/年份 | 状态 | 说明 |
|---|---|---|---|
| MyBatis ORM框架 | 3.5.x (2024) | ✅ 已验证 | 本文代码基于3.5.x版本 |
| MySQL数据库 | 8.0+ | ✅ 已验证 | 支持InnoDB引擎与AUTO_INCREMENT |
| Jdbc连接池 | POOLED | ✅ 已验证 | MyBatis内置连接池配置 |
| resultMap映射 | MyBatis 3.0+ | ✅ 已验证 | association标签实现嵌套结果映射 |
| 延迟加载 | MyBatis 3.0+ | ⚠️ 配置项 | 需在sqlMapConfig中开启fetchType |
| UNIQUE约束实现1对1 | MySQL 5.0+ | ✅ 已验证 | 外键+唯一索引方案 |
| 主键关联实现1对1 | MySQL 5.0+ | ✅ 已验证 | 共享主键方案 |
数据库中的 1 对 1 模型
在数据库设计中,1 对 1 模型(One-to-One Relationship) 是一种实体关系,用于表示两张表之间一条记录只能关联另一张表中的一条记录的关系。例如,一个用户对应一个身份证号,一个员工对应一个工位,这种关系在现实世界中非常常见。
从数据库建模的角度来看,1 对 1 关系通常通过主键关联 或外键加唯一约束来实现:
- 主键关联:两张表共享相同的主键值,表 B 的主键同时也是引用表 A 的外键。
- 外键 + 唯一约束:表 B 中有一个外键字段引用表 A 的主键,并且该字段上设置了唯一约束(UNIQUE),确保表 B 的每一行只能关联表 A 中的一行。
一对一关系在数据拆分、安全隔离、性能优化等场景中扮演着不可替代的角色。本文将从概念、场景、优缺点到代码实战,全面解析数据库中的 1 对 1 模型,并通过 MyBatis 框架演示完整的查询实现。
1 对 1 关系的特点
- 唯一性:表 A 中的每一行记录只能与表 B 中的一行记录关联,反之亦然。这是 1 对 1 关系最核心的特征。
- 严格的映射:两张表的数据关系是一一对应的,不存在一对多中的「一方拥有多方」的情况。
- 逻辑层面:通常用于将一个实体的不同属性分离存储,例如将用户的基本信息与敏感信息分表存放,既方便管理又提升安全性。
- 双向可选:在实际业务中,1 对 1 关系可以是双向强制的(双方都必须存在),也可以是单向可选的(一方可以没有对应的另一方)。
常见的应用场景
- 数据拆分 :用于将经常访问的字段和不经常访问的字段分离,以优化性能。例如,用户的基本信息和敏感信息可以分表存储。
- 表 1:用户信息(如
id、username、email、avatar) - 表 2:用户敏感信息(如
id、id_card、password_hash、real_name)
- 表 1:用户信息(如
- 权限控制:例如,存储一个用户及其角色权限配置,用户和权限之间通常是一对一关系。每个用户有且仅有一个权限配置记录,便于独立管理权限。
- 扩展功能:将一个主表的扩展数据存储到另一张表,以便灵活扩展数据结构。例如,电商系统中的商品表与商品详情表,商品基本信息频繁查询,而详情(描述、参数等)按需加载。
- 业务隔离:不同业务模块的数据虽然逻辑上属于同一实体,但物理上分表存储,便于不同团队维护。例如,用户账户信息由支付团队维护,用户个人资料由用户中心团队维护。
优点分析
- 模块化设计:将数据分成不同表,便于维护和管理。
- 安全性提升:敏感信息可以单独存储并设置更高的权限控制。
- 性能优化:分表存储减少单表的数据量,提升查询效率。
缺点分析
- 查询复杂性增加:需要进行表的连接查询(JOIN),可能增加性能开销。
- 维护难度加大:需要严格设计和维护表之间的关系,避免数据不一致。
- 事务管理成本:跨表操作需要在事务中处理,以确保数据一致性。
查询模型
用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户,此时查询一个订单,与此同时查询出该订单的所属用户。 
创建表
sql
CREATE TABLE `wzk_orders` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`ordertime` varchar(255) DEFAULT NULL,
`total` double DEFAULT NULL,
`uid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
CREATE TABLE `wzk_user` (
`id` int(11) NOT NULL,
`username` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`birthday` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
插入数据
wzk_user 表数据如下:
wzk_orders 表数据如下: 
查询语句
对应的 SQL 语句如下:
sql
select * from wzk_orders o, wzk_user u where o.uid=u.id;
查询结果如下所示: 
创建类
WzkOrder
java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class WzkOrder {
private int id;
private Date ordertime;
private double total;
private WzkUser user;
}
WzkUser
java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class WzkUser {
private int id;
private String username;
private String password;
private Date birthday;
}
OrderMapper
java
public interface OrderMapper {
List<WzkOrder> findAll();
}
OrderMapper.xml
xml
<?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="icu.wzk.mapper.OrderMapper">
<!-- 写法一 -->
<resultMap id="orderMap" type="icu.wzk.model.WzkOrder">
<result column="uid" property="user.id"></result>
<result column="username" property="user.username"></result>
<result column="password" property="user.password"></result>
<result column="birthday" property="user.birthday"></result>
</resultMap>
<!-- 写法二 -->
<!-- <resultMap id="orderMap" type="icu.wzk.model.WzkOrder">-->
<!-- <result property="id" column="id"></result>-->
<!-- <result property="ordertime" column="ordertime"></result>-->
<!-- <result property="total" column="total"></result>-->
<!-- <association property="user" javaType="icu.wzk.model.WzkUser">-->
<!-- <result column="uid" property="id"></result>-->
<!-- <result column="username" property="username"></result>-->
<!-- <result column="password" property="password"></result>-->
<!-- <result column="birthday" property="birthday"></result>-->
<!-- </association>-->
<!-- </resultMap>-->
<select id="findAll" resultMap="orderMap">
select * from wzk_orders o, wzk_user u where o.uid=u.id
</select>
</mapper>
sqlMapConfig.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties>
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://172.16.1.130:3306/wzk-mybatis?characterEncoding=utf-8"/>
<property name="user" value="hive"/>
<property name="password" value="hive@wzk.icu"/>
</properties>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driverClass}"/>
<property name="url" value="${jdbcUrl}"/>
<property name="username" value="${user}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper.xml"/>
<!-- 新增的 OrderMapper -->
<mapper resource="OrderMapper.xml"/>
</mappers>
</configuration>
编写代码
java
public class WzkIcu08 {
public static void main(String[] args) throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
OrderMapper orderMapper = sqlSession.getMapper(OrderMapper.class);
List<WzkOrder> dataList = orderMapper.findAll();
dataList.forEach(System.out::println);
sqlSession.close();
}
}
编写的代码如下图所示: 
运行结果
运行之后,对应的控制台输出结果如下:
shell
24/11/12 16:51:34 DEBUG OrderMapper.findAll: <== Total: 3
WzkOrder(id=1, ordertime=Mon Nov 11 00:00:00 CST 2024, total=100.0, user=WzkUser(id=1, username=wzk, password=icu, birthday=Mon Nov 11 00:00:00 CST 2024))
WzkOrder(id=2, ordertime=Mon Nov 11 00:00:00 CST 2024, total=200.0, user=WzkUser(id=1, username=wzk, password=icu, birthday=Mon Nov 11 00:00:00 CST 2024))
WzkOrder(id=3, ordertime=Sun Nov 10 00:00:00 CST 2024, total=150.0, user=WzkUser(id=2, username=wzk2, password=icu2, birthday=Mon Nov 11 00:00:00 CST 2024))
对应的截图如下所示: 
注意事项
- 数据一致性:必须确保表 A 和表 B 之间的引用完整性,避免孤立记录(Orphan Records)。建议使用外键约束或在应用层保证数据一致性。
- 索引设计:为外键列创建索引,以提升连接查询性能。尤其是在数据量较大的情况下,缺少索引会导致 JOIN 查询效率急剧下降。
- 场景适配:如果没有明显的 1 对 1 关系需求,建议尽量避免使用,以简化设计。很多时候,将两张表合并为一张表反而更简单高效。
- 事务管理:当需要同时操作两张表时(如同时插入用户信息和敏感信息),务必使用数据库事务,确保要么全部成功,要么全部回滚。
- 延迟加载:在 ORM 框架(如 MyBatis、Hibernate)中,对于不常用的关联数据,建议使用延迟加载(Lazy Loading),避免每次查询都 JOIN 不必要的表。
错误速查卡
| 症状 | 根因 | 定位 | 修复 |
|---|---|---|---|
| 查询结果 user 字段为 null | resultMap 中未配置 association 或 column 映射错误 | 检查 resultMap 的 property 与 javaType 是否匹配 | 添加 <association property="user" javaType="..."> 或确保 result column 正确映射 |
| SQL 能查出数据但对象属性为空 | resultMap 中 column 名与 SQL 查询列名不一致 | 对比 resultMap column 与 SELECT 语句中的列别名 | 确保 result column 值与 SELECT 返回的列名完全一致 |
| 运行报错 "Could not find result map" | resultMap id 拼写错误或未定义 | 检查 <select resultMap="orderMap"> 与 <resultMap id="orderMap"> 是否匹配 |
确认 resultMap id 拼写一致,注意大小写敏感 |
| 连接查询性能极慢 | 外键列 uid 未建索引 | 执行 EXPLAIN 查看查询计划 | 为 wzk_orders.uid 列添加索引 CREATE INDEX idx_uid ON wzk_orders(uid) |
| 事务未生效,数据只写入一张表 | 未开启事务或自动提交 | 检查 sqlSessionFactory.openSession() 是否未传 true | 使用 openSession(true) 开启自动提交,或手动调用 commit() |
| 延迟加载后 session 关闭导致懒加载异常 | 延迟加载的数据在 session 关闭后才访问 | 在 session 关闭后调用 order.getUser().getUsername() | 使用 OpenSessionInFilter 或确保在 session 生命周期内完成数据访问 |
作者:武子康的个人博客