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
相关产品推荐
相关产品推荐

