设计原则在数据库访问层中的应用:ORM框架的设计,Repository模式与DIP
说实话,数据库访问层这块,我见过太多“面条式”代码了。SQL语句满天飞,业务逻辑和数据库操作搅在一起,改个字段名能让你找遍整个项目。嗯,今天我们就聊聊怎么用设计原则来拯救数据库访问层。
ORM框架:对象和关系之间的“翻译官”
先说说ORM。ORM的全称是Object-Relational Mapping,说白了就是帮你把数据库里的表映射成内存里的对象。你想想看,关系数据库是二维表,而面向对象是继承、多态、封装,这俩天生就不对付。ORM就是中间的翻译官。
我个人习惯把ORM分成三层:
- 元数据映射层:定义表和类之间的对应关系
- 持久化层:负责CRUD操作
- 查询层:提供查询接口
我在项目中遇到过一种情况:团队自己手写了一个ORM,结果元数据映射写死在代码里。每次加个字段,要改三个地方。后来我重构时,引入了注解(Annotation)机制,用反射读取元数据,这才算解脱。
核心原则:ORM的设计要遵循“单一职责原则”。映射、持久化、查询,这三件事必须分开。别揉在一起。
Repository模式:隔离持久化细节
Repository模式,说白了就是给数据访问加个“中间层”。业务代码不直接操作ORM,而是通过Repository接口来访问数据。这样做的好处是什么?
- 业务代码不知道底层是MySQL还是PostgreSQL
- 换数据库时,只改Repository的实现
- 单元测试时,可以轻松Mock掉数据层
来看一个简单的例子:
// 定义Repository接口
class UserRepository {
public:
virtual ~UserRepository() = default;
virtual User findById(int id) = 0;
virtual void save(const User& user) = 0;
virtual void remove(int id) = 0;
};
// 实现一个基于ORM的版本
class OrmUserRepository : public UserRepository {
private:
DatabaseContext& context;
public:
OrmUserRepository(DatabaseContext& ctx) : context(ctx) {}
User findById(int id) override {
return context.query<User>()
.where("id = ?", id)
.first();
}
void save(const User& user) override {
context.persist(user);
}
void remove(int id) override {
context.remove<User>(id);
}
};
你看,业务代码只需要依赖UserRepository这个抽象接口,根本不用管底层怎么查的。我曾经在一个项目里,从SQLite切换到PostgreSQL,只改了一行依赖注入的代码。嗯,这就是Repository模式的价值。
小技巧:Repository接口的方法命名要贴近业务语言,比如findActiveUsers()而不是findByStatus(1)。这样业务代码读起来就像在说人话。
DIP:依赖倒置,让高层不依赖低层
DIP(Dependency Inversion Principle)告诉我们:高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
放到数据库访问层里,就是:
- 业务逻辑层(高层)不直接依赖ORM或数据库驱动(低层)
- 两者都依赖Repository接口(抽象)
- ORM的实现细节依赖Repository接口来适配
我画了一张图,帮你理清这个关系:
你看这张图,箭头指向的是被依赖的一方。业务层指向接口,实现层也指向接口。这样依赖关系就“倒置”了——不再是高层依赖低层,而是大家都依赖抽象。
注意:DIP不是让你把所有类都抽象一遍。过度抽象是种病。我见过有人给每个DAO都配一个接口,结果项目里接口比实现还多。记住,只有那些可能变化的部分才需要抽象。
实战:把原则串起来
好了,理论说完了。我们看看怎么在实际项目中把这些原则串起来。假设我们要做一个用户管理系统:
// 1. 定义领域对象
class User {
public:
int id;
std::string name;
std::string email;
// ... 其他字段
};
// 2. 定义Repository接口(抽象)
class UserRepository {
public:
virtual ~UserRepository() = default;
virtual User findById(int id) = 0;
virtual std::vector<User> findByName(const std::string& name) = 0;
virtual void save(User& user) = 0;
virtual void remove(int id) = 0;
};
// 3. 实现基于ORM的Repository
class OrmUserRepository : public UserRepository {
private:
OrmContext& orm;
public:
explicit OrmUserRepository(OrmContext& ctx) : orm(ctx) {}
User findById(int id) override {
return orm.query<User>()
.where("id = ?", id)
.execute()
.first();
}
// ... 其他方法实现
};
// 4. 业务服务只依赖接口
class UserService {
private:
UserRepository& repo;
public:
explicit UserService(UserRepository& r) : repo(r) {}
void changeEmail(int userId, const std::string& newEmail) {
auto user = repo.findById(userId);
user.email = newEmail;
repo.save(user);
}
};
这段代码里,UserService只依赖UserRepository接口。至于底层是MySQL还是PostgreSQL,是ORM还是手写SQL,UserService一概不知。这就是DIP的力量。
我的经验:依赖注入配合DIP使用效果最佳。把Repository的实现通过构造函数注入,而不是在业务代码里new出来。这样换实现时,只需要改注入的那一行。
避坑指南
我曾经在一个项目里踩过一个坑:团队为了“灵活”,给每个Repository都加了十几个方法。结果业务代码直接调用Repository的底层查询方法,绕过了业务逻辑。嗯,这其实违背了Repository模式的初衷。
记住:Repository的接口应该反映业务能力,而不是数据库操作。比如:
- ✅
findActiveUsers()— 业务语义 - ❌
findByStatus(int status)— 数据库语义
另一个常见问题是:把Repository和ORM混为一谈。Repository是模式,ORM是工具。你可以用ORM实现Repository,也可以用原生SQL实现。两者是不同层面的东西。
好了,今天的内容就到这里。记住三个关键词:ORM负责映射,Repository负责隔离,DIP负责解耦。这三板斧用好了,你的数据库访问层会干净很多。
公众号:蓝海资料掘金营,微信deep3321