查询结果为什么能自动变成 Java 对象?

上一篇我们讲了:

一条 SQL 在 MyBatis 里到底经历了什么?

最终执行链路:

MapperProxy
    ↓
SqlSession
    ↓
Executor
    ↓
StatementHandler
    ↓
Database

SQL 执行完成以后。

数据库返回:

ResultSet

新的问题来了,数据库返回的是表数据。

而业务代码拿到的却是对象。

例如数据库:

id=1
name=Tom
age=18

业务代码:

User user = userMapper.selectById(1L);

直接得到:

user.getName();

这是怎么做到的?

难道 MyBatis 会魔法?当然不是

今天就看看 MyBatis 最重要的能力之一:

结果映射


先想一个问题

如果不用 MyBatis,纯 JDBC 怎么写?

PreparedStatement ps =
    connection.prepareStatement(sql);

ResultSet rs = ps.executeQuery();

User user = new User();

if (rs.next()) {
    user.setId(rs.getLong("id"));
    user.setName(rs.getString("name"));
    user.setAge(rs.getInt("age"));
}

有没有觉得很烦?

每查一次数据,都要自己写映射逻辑,而且字段越多越痛苦。


MyBatis 帮你干了什么?

我们平时只写:

User user = userMapper.selectById(1L);

剩下的映射过程,全部由 MyBatis 自动完成。

核心组件:

ResultSetHandler

它专门负责:

ResultSet
      ↓
Java对象

转换。


ResultSet 到底长什么样?

假设数据库返回:

id name age
1 Tom 18

JDBC 拿到的是:

ResultSet

本质上可以理解成:

列名 → 值

例如:

id   → 1

name → Tom

age  → 18

但业务需要的是:

User

对象。

所以必须完成一次映射。


MyBatis 是怎么映射的?

假设实体:

public class User {

    private Long id;

    private String name;

    private Integer age;

}

数据库字段:

id
name
age

MyBatis 会发现:

字段名
==
属性名

于是直接反射赋值:

user.setId(1L);

user.setName("Tom");

user.setAge(18);

最终得到:

User

对象。


如果字段名不一样呢?

现实项目里经常出现:

数据库:

user_name

Java:

userName

这时候就匹配不上了。

怎么办,很多人知道:

map-underscore-to-camel-case: true

开启驼峰转换。

但很少有人知道原理,实际上 MyBatis 在映射时。

会把:

user_name

转换成:

userName

然后继续完成属性匹配。

所以:

数据库字段
      ↓
驼峰转换
      ↓
Java属性

这就是自动映射的本质。


自动映射真的可靠吗?

很多人觉得:

字段一样
自动映射
结束

其实没这么简单。

例如:

select
id,
name
from user

返回:

User

其中:

age

没有查询出来,怎么办?

MyBatis 会自动跳过,不会报错。


反过来。

如果 SQL 返回:

nickname

而对象里没有:

nickname

对应属性,MyBatis 同样会忽略。

所以自动映射容错性非常高。


复杂对象怎么办?

简单对象很好处理,那这种呢?

public class Order {

    private Long id;

    private User user;

}

数据库返回:

order_id

user_id

user_name

此时已经不是简单映射了。

MyBatis 引入:

ResultMap

解决。

很多人只知道 ResultMap 很强,但不知道为什么存在。

原因很简单:自动映射只能处理简单对象,复杂对象必须告诉 MyBatis:

哪个字段

对应哪个属性

所以:

ResultMap

本质上是一份映射规则。


为什么 MyBatis 要设计 ResultMap?

因为自动映射终究有极限。

例如:

订单
   ↓
用户
      ↓
角色

这种嵌套对象,靠字段名猜测已经不现实了,必须有明确映射关系。

于是:

ResultMap

诞生了。

官方甚至说过一句话:

ResultMap 是 MyBatis 最强大的特性之一。


看一眼源码

数据库查询完成后。

最终进入:

DefaultResultSetHandler

核心方法:

handleResultSets()

负责处理:

ResultSet

然后:

createResultObject()

创建对象。

接着:

applyAutomaticMappings()

自动映射字段。

如果配置了:

ResultMap

则进入:

applyPropertyMappings()

按照映射规则赋值。

最终得到:

User

Order

Product

等业务对象。


总结

很多人以为:

User user =
    userMapper.selectById(1L);

返回对象是一件理所当然的事情。

实际上背后经历了完整转换过程:

ResultSet
      ↓
ResultSetHandler
      ↓
字段匹配
      ↓
反射赋值
      ↓
Java对象

简单场景:

自动映射

复杂场景:

ResultMap

所以:

MyBatis 之所以能把查询结果自动变成 Java 对象,本质上是因为 ResultSetHandler 完成了数据库结果与 Java 对象之间的映射。

这也是 MyBatis 比 JDBC 好用得多的重要原因之一。


上一篇:

《一条 SQL 在 MyBatis 里到底经历了什么?》

下一篇:

《MyBatis 动态 SQL 为什么能如此灵活?》

更多推荐