数据映射器模式:让对象与数据库彻底解耦
数据映射器(Data Mapper)这个模式,说白了就是给对象和数据库之间加一层“翻译官”。对象只管自己是啥,数据库只管数据怎么存,中间怎么转换、怎么同步,全交给映射器去处理。
我在做企业级项目时,最头疼的就是业务代码里到处是SQL。改个字段名,得翻遍几十个文件。后来用了数据映射器,世界清净了。你想想看,对象和数据库之间有了清晰的边界,维护起来多舒服。
核心思想:分离关注点
数据映射器的本质,就是把“对象如何持久化”这个职责,从对象本身剥离出去。对象不需要知道自己怎么存到数据库,映射器也不需要关心业务逻辑。
嗯,这里要注意:数据映射器和Active Record模式正好相反。Active Record让对象自己负责持久化,比如Rails里的模型。而数据映射器把持久化逻辑单独抽出来,对象就是个纯POJO。
- 实体对象(Entity):纯业务对象,不包含任何持久化逻辑
- 映射器接口(Mapper Interface):定义CRUD操作的契约
- 具体映射器(Concrete Mapper):实现映射逻辑,处理对象与数据库的转换
一个简单的例子:用户管理
先看实体对象。它就是个普通的Java Bean,没有任何框架注解,也没有继承什么基类。
// 实体对象 - 纯POJO
public class User {
private Long id;
private String username;
private String email;
private LocalDateTime createdAt;
// 构造器、getter/setter
public User() {}
public User(String username, String email) {
this.username = username;
this.email = email;
this.createdAt = LocalDateTime.now();
}
// getter/setter 省略...
}
然后是映射器接口。它定义了操作,但不关心具体怎么实现。
// 映射器接口
public interface UserMapper {
User findById(Long id);
List<User> findAll();
void insert(User user);
void update(User user);
void delete(Long id);
}
具体映射器负责实现。这里我用JDBC做演示,实际项目中可能是MyBatis的XML映射文件。
// 具体映射器 - JDBC实现
public class UserMapperJdbcImpl implements UserMapper {
private DataSource dataSource;
public UserMapperJdbcImpl(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public User findById(Long id) {
String sql = "SELECT id, username, email, created_at FROM users WHERE id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return mapRow(rs);
}
}
} catch (SQLException e) {
throw new RuntimeException("Failed to find user by id: " + id, e);
}
return null;
}
@Override
public void insert(User user) {
String sql = "INSERT INTO users (username, email, created_at) VALUES (?, ?, ?)";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, user.getUsername());
ps.setString(2, user.getEmail());
ps.setTimestamp(3, Timestamp.valueOf(user.getCreatedAt()));
ps.executeUpdate();
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) {
user.setId(rs.getLong(1));
}
}
} catch (SQLException e) {
throw new RuntimeException("Failed to insert user", e);
}
}
// 其他方法实现类似...
private User mapRow(ResultSet rs) throws SQLException {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setEmail(rs.getString("email"));
user.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
return user;
}
}
数据映射器的核心流程图
下面这张图展示了数据映射器的工作流程。业务层只和映射器接口打交道,映射器负责和数据库交互。
MyBatis中的Mapper原理
说到数据映射器,就不得不提MyBatis。MyBatis的Mapper,本质上就是数据映射器模式的一种实现。但它的做法更巧妙——用动态代理和XML/注解来生成映射器实现。
我记得第一次看MyBatis源码时,被它的MapperProxy惊艳到了。你只需要写一个接口,配上XML或注解,MyBatis就自动帮你生成实现类。这不就是数据映射器模式的最佳实践吗?
MyBatis Mapper的工作机制
- 注册映射器接口:通过
mapper标签或@MapperScan注册 - 解析映射文件:读取XML或注解中的SQL语句
- 生成代理对象:使用JDK动态代理创建接口的代理实例
- 执行SQL:代理对象拦截方法调用,执行对应的SQL并映射结果
// MyBatis映射器接口示例
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User findById(@Param("id") Long id);
@Insert("INSERT INTO users(username, email, created_at) VALUES(#{username}, #{email}, #{createdAt})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
@Update("UPDATE users SET username = #{username}, email = #{email} WHERE id = #{id}")
int update(User user);
@Delete("DELETE FROM users WHERE id = #{id}")
int delete(Long id);
}
| 对比项 | 手写映射器 | MyBatis Mapper |
|---|---|---|
| 实现方式 | 手动编写实现类 | 动态代理自动生成 |
| SQL管理 | 散落在Java代码中 | 集中到XML或注解 |
| 结果映射 | 手动ResultSet转换 | 自动映射或配置映射 |
| 灵活性 | 完全可控 | 约定优于配置 |
数据映射器的优缺点
任何模式都有两面性。数据映射器也不例外。
优点
- 解耦彻底:业务对象完全不依赖持久化框架
- 测试方便:可以轻松mock映射器,单元测试好写
- 职责清晰:每个组件只做一件事
- 可替换性强:换数据库或ORM框架,只改映射器实现
缺点
- 代码量增加:需要为每个实体写映射器接口和实现
- 学习成本:团队成员需要理解这种分层思想
- 性能开销:多了一层抽象,但通常可以忽略
什么时候用数据映射器?
我个人习惯在以下场景使用数据映射器:
- 复杂业务系统:业务逻辑和持久化需要清晰分离
- 多数据库支持:需要切换MySQL、PostgreSQL等
- 领域驱动设计:DDD中聚合根需要纯领域对象
- 测试驱动开发:需要频繁mock持久层
如果是简单项目,或者团队规模小,用Active Record模式可能更省事。别为了用模式而用模式,这是很多新手容易犯的错。
数据映射器模式,说白了就是给对象和数据库之间加一层清晰的边界。这层边界让两边各自专注自己的事,互不干扰。MyBatis把这个模式发挥到了极致,用动态代理省去了大量样板代码。
嗯,最后提醒一句:模式是工具,不是目的。理解它的思想,然后在合适的场景用合适的工具,这才是架构师该有的思维方式。
公众号:蓝海资料掘金营,微信deep3321