重构第65章:重构与数据库访问——引入Repository模式
说实话,很多项目的数据库访问层,最后都变成了“大泥球”。
业务逻辑里直接塞SQL,查询散落在各个Controller里,想换个ORM?那简直是噩梦。我见过一个项目,光改个数据库连接池,就改了三天——因为SQL写得太“野”了。
这一章,我们来聊聊怎么用Repository模式,把数据访问层从“野路子”拉回正轨。
为什么需要Repository模式?
先问个问题:你的业务代码里,是不是经常出现这样的代码?
// 业务逻辑里直接写SQL
public Order getOrder(Long id) {
String sql = "SELECT * FROM orders WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{id}, orderRowMapper);
}
这有什么问题?
- 耦合太深:业务逻辑和数据库实现绑死了。
- 测试困难:想单元测试?得先启动数据库。
- 切换成本高:从MySQL换到PostgreSQL?所有SQL都得改。
我个人习惯,把数据访问抽象成接口。业务层只跟接口打交道,底层实现随便换。这就是Repository模式的核心思想。
核心定义:Repository模式在领域层和数据映射层之间建立了一个中间层。它让业务代码感觉“就像在操作一个内存中的集合”,而不是在操作数据库。
重构前的“大泥球”
先看一个典型的“坏味道”代码。假设我们有个用户管理系统,业务逻辑和数据库访问混在一起:
public class UserService {
private JdbcTemplate jdbcTemplate;
public User findUserById(Long id) {
// 直接写SQL
String sql = "SELECT id, name, email FROM users WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{id}, (rs, rowNum) -> {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
return user;
});
}
public void saveUser(User user) {
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
jdbcTemplate.update(sql, user.getName(), user.getEmail());
}
// 还有更多方法... 全都混在一起
}
嗯,这里要注意。这个类看起来“功能完整”,但一旦需求变复杂,它就会迅速膨胀。我在项目中遇到过类似的情况——一个Service类最后写了3000行,里面一半是SQL,一半是业务逻辑。改一个查询,得从头读到尾,生怕改错。
引入Repository模式:三步走
重构其实不复杂。分三步走:
- 定义Repository接口:抽象出数据访问的契约。
- 实现Repository接口:把具体的SQL实现藏起来。
- 修改业务层:让业务代码只依赖接口。
第一步:定义接口
public interface UserRepository {
User findById(Long id);
void save(User user);
void deleteById(Long id);
List<User> findAll();
}
你看,这个接口里没有任何SQL的影子。业务层看到的就是“我要找用户”、“我要保存用户”。至于底层是MySQL还是Redis,业务层不关心。
第二步:实现接口
@Repository
public class JdbcUserRepository implements UserRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcUserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public User findById(Long id) {
String sql = "SELECT id, name, email FROM users WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{id}, this::mapRowToUser);
}
@Override
public void save(User user) {
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
jdbcTemplate.update(sql, user.getName(), user.getEmail());
}
// 其他实现...
private User mapRowToUser(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
return user;
}
}
SQL被封装在实现类里。想换ORM?比如换成MyBatis?再写一个MyBatisUserRepository就行。业务层完全不用动。
第三步:改造业务层
public class UserService {
private final UserRepository userRepository;
// 依赖注入接口,而不是具体实现
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(Long id) {
return userRepository.findById(id);
}
public void registerUser(User user) {
// 业务逻辑:检查邮箱是否重复
// 然后调用Repository保存
userRepository.save(user);
}
}
业务层干净了。测试也简单了——传一个Mock的UserRepository进去就行。
小技巧:我建议在接口命名上保持一致性。比如都用findById、save、delete,而不是有的叫getUser,有的叫queryUser。团队里统一命名规范,能省很多沟通成本。
Repository模式的核心结构
为了让你更直观地理解,我画了一张图:
说白了,Repository模式就是加了一层“中间人”。业务层不直接跟数据库打交道,而是通过这个中间人。中间人负责翻译——把业务层的“我要找用户”翻译成“SELECT * FROM users”。
避坑指南
我曾经踩过一个坑:把Repository做得太“重”。
什么意思呢?就是在一个Repository里塞了太多方法。比如findByNameAndAge、findByNameAndEmail、findByNameAndAgeAndEmail……最后接口膨胀到几十个方法。
怎么避免?
- 用查询对象(Specification):把查询条件封装成一个对象,Repository只提供一个通用的
findBySpecification方法。 - 分拆Repository:如果一个Repository的方法超过10个,考虑拆分成多个小接口。比如
UserQueryRepository和UserCommandRepository。
注意:不要为了用模式而用模式。如果你的项目只有三张表,业务逻辑极其简单,直接用JPA的CrudRepository就够了。过度设计也是坏味道。
什么时候该用?
| 场景 | 推荐使用? | 原因 |
|---|---|---|
| 项目有复杂的业务逻辑 | ✅ 强烈推荐 | 业务层需要隔离数据访问细节 |
| 可能切换数据库或ORM | ✅ 强烈推荐 | 切换成本极低,只需新增实现 |
| 需要单元测试 | ✅ 推荐 | 可以轻松Mock Repository |
| 简单的CRUD项目 | ⚠️ 谨慎使用 | 可能过度设计,增加复杂度 |
你想想看,如果你的项目未来可能要换数据库,或者你的业务逻辑复杂到需要大量单元测试,那Repository模式几乎是必选项。
这一章的内容就到这里。记住:重构不是炫技,是为了让代码更容易维护、更容易测试。Repository模式只是工具,用对了地方,它就是利器。
一句话总结:Repository模式把数据访问抽象成接口,让业务层只关心“做什么”,不关心“怎么做”。