设计原则在数据库访问层中的应用: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接口来适配

我画了一张图,帮你理清这个关系:

依赖倒置在数据库访问层中的应用 业务逻辑层 (高层模块) Repository接口 (抽象) ORM实现 (低层模块) 依赖 实现 关键点说明: • 业务逻辑层只依赖 Repository 接口,不依赖具体 ORM • ORM 实现必须遵循 Repository 接口的约定 • 更换数据库或 ORM 时,只需新增一个实现类 • 单元测试时,可以 Mock 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