You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

RAII不可用时,如何确保数据库字段修改后自动触发更新事件?

自动触发数据库字段修改事件的实现方案

背景

我们有一个存储多类值的数据库,多个类可修改其中的值,且每次修改必须触发事件通知其他类。当前使用如下包装器实现:

struct Database {
    std::string s;
    int i;
};

template <auto PMD>
struct DbFieldAccessor {
    // 模板逻辑,从(PMD = T C::*)提取T类型
    using FieldType = PmdFieldType_t<decltype(PMD)>;

    explicit DbFieldAccessor(Database& database) : m_database{&database} {}

    constexpr auto operator->() noexcept -> FieldType* {
        m_modified = true;
        return &(m_database->*PMD);
    }

    auto isFieldModified() const noexcept -> bool { return m_modified; }

   private:
    Database* m_database;
    bool m_modified = false;
};

template <class Db>
class DatabaseAdapter {
   public:
    explicit DatabaseAdapter(Db db) : m_database{std::move(db)} {}

    template <auto PMD>
    [[nodiscard]] auto mutableAccess() -> DbFieldAccessor<PMD> {
        return DbFieldAccessor<PMD>{m_database};
    }
    template <auto... PMDs>
    auto emitUpdates(const DbFieldAccessor<PMDs>&... accessors) -> void {
        (
            [](const auto& accessor) {
                if (accessor.isFieldModified()) {
                    // emitEvent(events::FieldUpdated<PMDs>{});
                }
            }(accessors),
            ...);
    }

   private:
    Db m_database{};
};

预期用法

Database internalDb; // 绝不直接访问
DatabaseAdapter<Database> db{internalDb};

auto access = db.mutableAccess<&Database::s>(); // 获取Database中s的访问权
access->push_back('D'); // 修改字段
db.emitUpdates(access); // 触发更新事件

当前问题

修改字段后极易忘记调用emitUpdates,我们尝试过三种方案但均有缺陷:

  • RAII方案:在~DbFieldAccessor中调用emitUpdates不可行,因为emitUpdates可能抛出异常;
  • 函数包装方案:会产生不必要的拷贝,且使用不便(例如原本的push_back操作需改为传入getFromDb(s) + 'D'):
    template<auto PMD, class Db>
    void modifyDb(DatabaseAdapter<Db>& db, const PmdFieldType_t<decltype(PMD)>& data)
    {
        auto access = db.template mutableAccess<PMD>();
        *access = data;
        db.emitUpdates(dispatcher, entityId, access);
    }
    
  • 传入Lambda修改方案:调用处存在冗余代码,若使用auto&作为Lambda参数则会丢失代码补全功能:
    template<auto PMD, class Db, class Func>
    void modifyDb(DatabaseAdapter<Db>& db, Func&& func)
    {
        auto access = db.template mutableAccess<PMD>();
        std::forward<Func>(func)(*access);
        db.emitUpdates(dispatcher, entityId, access);
    }
    // 调用处
    modifyDb<&Database::s>(db, [](auto& s){s.push_back('D');});
    

请问还有其他可行的实现方案吗?所有代码均由我们维护,可进行任意修改。


可行方案推荐

方案1:改进Lambda封装,保留代码补全

通过显式指定Lambda参数类型解决代码补全问题,同时简化调用语法:

首先给DbFieldAccessor添加获取字段引用的方法:

template <auto PMD>
struct DbFieldAccessor {
    // ... 原有代码 ...
    auto get() noexcept -> FieldType& {
        m_modified = true;
        return m_database->*PMD;
    }
};

然后修改modifyDb模板,让它能推导字段类型,同时提供辅助模板简化调用:

template <class Db, auto PMD, class Func>
void modifyDb(DatabaseAdapter<Db>& db, Func&& func) {
    auto access = db.template mutableAccess<PMD>();
    std::forward<Func>(func)(access.get());
    db.emitUpdates(access);
}

// 辅助模板,用于推导Db类型
template <auto PMD, class Func, class Db>
void modifyDb(DatabaseAdapter<Db>& db, Func&& func) {
    modifyDb<Db, PMD>(db, std::forward<Func>(func));
}

调用时显式指定字段类型,让IDE提供代码补全:

modifyDb<&Database::s>(db, [](std::string& s) {
    s.push_back('D'); // 正常触发代码补全
});

方案2:使用代理对象+显式提交

将DbFieldAccessor改为代理对象,拦截所有修改操作并标记状态,通过显式提交触发事件,既避免析构抛异常,又降低遗漏概率:

template <auto PMD>
struct DbFieldProxy {
    using FieldType = PmdFieldType_t<decltype(PMD)>;

    explicit DbFieldProxy(DatabaseAdapter<Database>& adapter, Database& db)
        : m_adapter{&adapter}, m_db{&db}, m_modified{false}, m_original{db.*PMD} {}

    // 重载赋值操作
    DbFieldProxy& operator=(const FieldType& val) {
        m_db->*PMD = val;
        m_modified = true;
        return *this;
    }

    DbFieldProxy& operator=(FieldType&& val) noexcept {
        m_db->*PMD = std::move(val);
        m_modified = true;
        return *this;
    }

    // 提供modify方法处理成员函数式修改(如push_back)
    template <class Func>
    void modify(Func&& func) {
        func(m_db->*PMD);
        m_modified = true;
    }

    // 显式提交修改,触发事件
    void commit() {
        if (m_modified) {
            m_adapter->emitUpdates(*this);
            m_modified = false;
        }
    }

    // 析构时自动回滚未提交的修改(可选,根据业务需求)
    ~DbFieldProxy() noexcept {
        if (m_modified) {
            m_db->*PMD = std::move(m_original);
        }
    }

    auto isModified() const noexcept -> bool { return m_modified; }

private:
    DatabaseAdapter<Database>* m_adapter;
    Database* m_db;
    bool m_modified;
    FieldType m_original; // 保存原始值用于回滚
};

// 修改DatabaseAdapter的mutableAccess返回代理
template <class Db>
class DatabaseAdapter {
public:
    // ... 原有代码 ...
    template <auto PMD>
    [[nodiscard]] auto mutableAccess() -> DbFieldProxy<PMD> {
        return DbFieldProxy<PMD>{*this, m_database};
    }

    // 适配emitUpdates接收DbFieldProxy
    template <auto... PMDs>
    auto emitUpdates(const DbFieldProxy<PMDs>&... proxies) -> void {
        (
            [](const auto& proxy) {
                if (proxy.isModified()) {
                    // emitEvent(events::FieldUpdated<PMDs>{});
                }
            }(proxies),
            ...);
    }
};

调用方式:

auto proxy = db.mutableAccess<&Database::s>();
proxy.modify([](std::string& s) { s.push_back('D'); });
proxy.commit(); // 显式提交,语义清晰,不易遗漏

方案3:编译期强制检查(进阶)

利用C++20概念或静态断言,结合类型系统确保DbFieldAccessor的使用必须伴随emitUpdates调用。核心思路是让DbFieldAccessor的修改接口仅能在emitUpdates的上下文生效,或通过模板元编程追踪调用流程。这种方案能从编译层面避免遗漏,但实现复杂,对现有代码侵入性较大,适合对安全性要求极高的场景。


内容的提问来源于stack exchange,提问作者Yksisarvinen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 17:35:00