oat++框架应用之do、dao、service

发布时间:2026/7/29 15:38:35
oat++框架应用之do、dao、service 引言本篇文章将重点不再放在oat的框架上而是用实际的例子讲述如何用框架构建一个后端交互的一个模型。基本概念do这是一个我们用代码写的一个数据库我们mysql里面存的字段在这个文件里面就会被转化成一个类对象所以这个类对象必须要包含所有mysql的字段。daodata access object 这个类我们要配合mapper来使用首先dao里面定义的是几个封装的函数这些函数就是用来和数据库来打交道的比如要访问数据库新增数据啥的所有的sql语句都是在dao里面的service负责和业务打交道也就是当前端数据传递给controller之后controller会转换成DTO然后调用service这一层就会把DTO变成DO然后调用DAO来操作数据库。mapper这个是和dao一起的主要是映射功能把前端的信息映射成一个对象代码DO我们还是先从DO开始说起DO的代码只有一件事情就是和数据库打交道数据库里面的所有字段都要被存储下来。我们为了代码的书写方便先设置了一个宏来方便获取对应字段的一个指针然后就是要添加对应表的所有字段。首先我们这里因为不可以有字段的重复所以在添加的时候一个是判断是不是主键一个是判断是不是又重复如果有那么就删除了再添加。void BaseDO::addColField(DOField* field) { // 判断是否是主键域 if (field-getIsPrimaryKey()) { if (_primaryField) delete _primaryField; _primaryField field; return; } // 如果已经存在字段域则删除 auto iter std::find_if(_fields.begin(), _fields.end(), [field](DOField* f) { return field-getColumn() f-getColumn(); }); if (iter ! _fields.end()) { delete* iter; _fields.erase(iter); } // 添加字段域 _fields.emplace_back(field); }然后我们的DO使用了ORM的思想也就是不止是专门的存储数据还可以动态的生成sql语句意思就是我只需要给出我的操作insert我的字段id就可以动态的组装一个sql语句出来但是调用这些sql语句的地方还是daoclass DOField final { public: // 字段值获取函数 using ValueGetter std::functionvoid* (); // 构造函数 DOField(std::string column, std::string type, ValueGetter getter, bool isNullable true, bool isPrimaryKey false); // 移动构造函数 DOField(DOField other) noexcept; // 删除拷贝构造/赋值 DOField(const DOField) delete; DOField operator(const DOField) delete; // 获取字段名称 std::string getColumn() const; // 获取字段类型占位符 std::string getType() const; // 获取是否为主键 bool getIsPrimaryKey() const; // 获取字段是否允许为空 bool getIsNullable() const; // 获取字段值指针 void* get(); // 获取字段值 templatetypename T const T* getValue(); private: // 字段名称 std::string column; // 字段类型占位符如s、i、b、d、dt std::string type; // 字段是否是主键 bool isPrimaryKey; // 字段是否允许为空 bool isNullable; // 获取字段值的函数 ValueGetter getter; };这就是最后完整的do了其实就是一个c型的数据库#define MYSQL_ADD_FIELD(__COL_NAME__, __COL_TYPE__, __PROP_VAR__) \ addColField(new DOField(__COL_NAME__, __COL_TYPE__, [this]() {return __PROP_VAR__.get(); })) class SampleDO : public BaseDO { // 编号 MYSQL_SYNTHESIZE(string, id, Id); // 姓名 MYSQL_SYNTHESIZE(string, name, Name); // 性别 MYSQL_SYNTHESIZE(string, sex, Sex); // 年龄 MYSQL_SYNTHESIZE(int, age, Age); // 创建人 MYSQL_SYNTHESIZE(string, createBy, CreateBy); // 创建时间 MYSQL_SYNTHESIZE(string, createTime, CreateTime); // 修改人 MYSQL_SYNTHESIZE(string, updateBy, UpdateBy); // 修改时间 MYSQL_SYNTHESIZE(string, updateTime, UpdateTime); public: SampleDO() : BaseDO(sample) { MYSQL_ADD_FIELD_PK(id, s, id); MYSQL_ADD_FIELD(name, s, name); MYSQL_ADD_FIELD(sex, s, sex); MYSQL_ADD_FIELD(age, i, age); MYSQL_ADD_FIELD(create_by, s, createBy); MYSQL_ADD_FIELD(create_time, s, createTime); MYSQL_ADD_FIELD(update_by, s, updateBy); MYSQL_ADD_FIELD(update_time, s, updateTime); } }; // 给SampleDO智能指针设定一个别名方便使用 typedef std::shared_ptrSampleDO PtrSampleDO;DAOdao的作用就是用sql语句和数据库进行交互首先我们会动态的生成一个查询器因为sql语句语法的限制我们必须要在WHERE 后面用AND所以这里我们统一生成一些查询的匹配条件比如idname啥的然后就是一些封装的函数就是我们查询的方式。这里所有的查询参数都是query里面已经定义好的。也就是我上一篇文章里面的代码class SampleDAO : public BaseDAO { private: //************************************ // Method: queryConditionBuilder // FullName: SampleDAO::queryConditionBuilder // Access: private // Returns: std::string 返回条件SQL语句即WHERE部分的语句 // Parameter: const SampleQuery::Wrapper query 查询数据对象 // Parameter: SqlParams params 存放查询数据的参数对象 // Description: 查询条件构建器根据SampleQuery对象动态构建查询条件相关参数 //************************************ inline std::string queryConditionBuilder(const SampleQuery::Wrapper query, SqlParams params); public: // 统计数据条数 uint64_t count(const SampleQuery::Wrapper query); // 分页查询数据 std::listSampleDO selectWithPage(const SampleQuery::Wrapper query); // 通过姓名查询数据 std::listSampleDO selectByName(const string name); // 通过ID查询数据 PtrSampleDO selectById(std::string id); };不过这里有一个问题就是我们查询了这么多的数据之后我们要返回一个DO的对像。但是如果我们无法直接利用我们刚刚写的DO因为那里面的数据太多了我们查询的仅仅是其中的几个数据而已所以我们就有了mapper这个类继承了DO首先里面专门创建了一个data这个data里面只有我们需要查询的字段也就是select后面需要写入的字段作用就是我们执行查询的sql命令之后要以DO的形式返回然后在service里面把DO转化成DTODTO再变成VO传递给前端class InventoryMaterialMapper : public MapperInventoryMaterialDO { public: InventoryMaterialDO mapper(ResultSet* resultSet) const override { InventoryMaterialDO inventoryMaterialDO; inventoryMaterialDO.setId(resultSet-getString(id)); inventoryMaterialDO.setCreatedBy(resultSet-getString(created_by)); inventoryMaterialDO.setUpdatedBy(resultSet-getString(updated_by)); inventoryMaterialDO.setBusNo(resultSet-getString(bus_no)); return inventoryMaterialDO; } };首先这个是动态sql语句的函数目的就是增加查询的条件我们根据查询的信息比如查询了id那么query里面的id就会存在于是我们增加AND id 这个只是一个占位符我们后面的SQLPARAMS_PUSH 会根据传入的数据把信息填入这个问号。首先我们的查询sql参数其实是一个数组这个数组里面存了两个元素一个是占位符因为我们传递的数据类型不同还有一个是值。class SqlParam { public: SqlParam(std::string key, shared_ptrvoid value) { this-key key; this-value value; } // 数据类型占位符 std::string key; // 对应的数据 shared_ptrvoid value; }; // 定义一个SQL填充参数数据集类型 typedef std::vectorSqlParam SqlParams;#define SQLPARAMS_PUSH(__PARAMS__, __PARAM_KEY__, __VAL_TYPE__, __VAL_VAL__) \ __PARAMS__.emplace_back(SqlParam(__PARAM_KEY__, std::make_shared__VAL_TYPE__(__VAL_VAL__)))注意一下这里的params是引用也就是说我们在这个函数里面才对这个params进行了填充这个params实际上是一个数组我们把查询的参数全部都推进了这个数组之中。std::string SampleDAO::queryConditionBuilder(const SampleQuery::Wrapper query, SqlParams params) { stringstream sqlCondition; sqlCondition WHERE 11; if (query-name) { sqlCondition AND name?; SQLPARAMS_PUSH(params, s, std::string, query-name.getValue()); } if (query-sex) { sqlCondition AND sex?; SQLPARAMS_PUSH(params, s, std::string, query-sex.getValue()); } if (query-age) { sqlCondition AND age?; SQLPARAMS_PUSH(params, i, int, query-age.getValue(0)); } return sqlCondition.str(); }然后就是我们的一些基本的查询sql语句了大家应该都注意到了返回的时候我们都是以mapper的形式返回的也就是最后返回了一个带有我们需要的数据的DO。std::listSampleDO SampleDAO::selectWithPage(const SampleQuery::Wrapper query) { SqlParams params; string sql SELECT id,name,sex,age FROM sample ; // 构建查询条件 sql queryConditionBuilder(query, params); // 构建排序语句 sql ORDER BY IFNULL(update_time, create_time) DESC, id DESC ; // 构建分页条件 sql LIMIT std::to_string(((query-pageIndex - 1) * query-pageSize)) , std::to_string(query-pageSize); // 执行查询 return sqlSession-executeQuerySampleDO(sql, SampleMapper(), params); } std::listSampleDO SampleDAO::selectByName(const string name) { string sql SELECT id,name,sex,age FROM sample WHERE name LIKE CONCAT(%,?,%); return sqlSession-executeQuerySampleDO(sql, SampleMapper(), %s, name); } PtrSampleDO SampleDAO::selectById(std::string id) { string sql SELECT id,name,sex,age FROM sample WHERE id?; return sqlSession-executeQueryOnePtrSampleDO(sql, PtrSampleMapper(), %s, id); }当然我们查询数据库大多情况下需要连表查询所以我这里的建议是再建立一个统一的mapper里面包含了所有的表最后返回这个mapper即可下面是一个增加的函数案例也就是dao这里就是sql的语句来和数据库打交道增删改查bool InventoryMaterialDAO::insert( InventoryMaterialDO obj ) { string sql INSERT INTO inventory_lock (inventory_id,bus_no,status) VALUES(?,?,?); SqlParams params; SQLPARAMS_PUSH( params, i, int32_t, obj.getInventoryId() ); SQLPARAMS_PUSH( params, s, string, obj.getBusNo() ); SQLPARAMS_PUSH( params, s, string, obj.getStatus() ); return sqlSession-executeUpdate( sql, params ); }service随后就是service部分这一部分串联了各种数据的转化。class SampleService { public: // 分页查询所有数据 SamplePageDTO::Wrapper listAll(const SampleQuery::Wrapper query); // 通过ID查询单个数据 SampleDTO::Wrapper getById(std::string id); // 保存数据 std::string saveData(const SampleAddDTO::Wrapper dto); // 批量保存数据 bool saveDataBatch(const oatpp::ListSampleAddDTO::Wrapper datas); // 修改数据 bool updateData(const SampleDTO::Wrapper dto); // 通过ID删除数据 bool removeData(const oatpp::Listoatpp::String ids); };这个是页查询的函数返回的也是页DTO可能大家会认为DTO和query内容都是差不多的但是其实有很大的区别首先DTO的数据一般会比query要多因为DTO是传输数据用的说实话query也可以拿DTO来查询但是DTO的数据会比较杂。所以我们任何传输数据都是用DTO如果有查询的就用query比如判断是不是查询了idname什么的。首先我们要创建一个DTO的页对象并初始化一些值然后我们根据查询的信息拿到对应的DO数据用dao调用查询的函数返回DO对象但是我们传递的时候统统用DTO所以最后我们一个一个的全部转化成DTO然后把我们刚刚生成的一个对象pages赋值进去。SamplePageDTO::Wrapper SampleService::listAll(const SampleQuery::Wrapper query) { // 构建返回对象 auto pages SamplePageDTO::createShared(); pages-pageIndex query-pageIndex; pages-pageSize query-pageSize; // 查询数据总条数 SampleDAO dao; uint64_t count dao.count(query); if (count 0) { return pages; } // 分页查询数据 pages-total count; pages-calcPages(); listSampleDO result dao.selectWithPage(query); // 将DO转换成DTO for (SampleDO sub : result) { auto dto SampleDTO::createShared(); ZO_STAR_DOMAIN_DO_TO_DTO(dto, sub, id, Id, name, Name, sex, Sex, age, Age); pages-addData(dto); } return pages; }这个是查询单一的数据依然返回DTO还是原来的思维用dao调用查询函数最后把返回的do变成dto用来传递SampleDTO::Wrapper SampleService::getById(std::string id) { // 查询数据 SampleDAO dao; auto res dao.selectById(id); // 没有查询到数据 if (!res) return nullptr; // 查询到数据转换成DTO auto dto SampleDTO::createShared(); ZO_STAR_DOMAIN_DO_TO_DTO_1(dto, res, id, Id, name, Name, sex, Sex, age, Age); return dto; }这个是保存单个数据这个和查询有些区别因为我们先需要组装成DO然后生成主键的id毕竟这个是数据的唯一标识原因就是dao操控的就是DO类型。然后设置一堆DTO里面没有而DO里面有的数据最后再调用dao来插入。std::string SampleService::saveData(const SampleAddDTO::Wrapper dto) { // 组装DO数据 SampleDO data; ZO_STAR_DOMAIN_DTO_TO_DO(data, dto, Name, name, Sex, sex, Age, age); // 生成ID UuidFacade uf; data.setId(uf.genUuid()); // 设置创建人 data.setCreateBy(dto-getPayload()-getUsername()); // 设置创建时间 data.setCreateTime(SimpleDateTimeFormat::format()); // 执行数据添加 SampleDAO dao; return dao.insert(data) 1 ? data.getId() : ; }这个是每一个dao继承的dao基类然后里面就是插入的sql语句。这个sql的构建INSERT INTO table (id, name) VALUES (123, zhangsan)所以我们val处理的是VALUES后面的col处理的是table后面的两个都是成对配对的当然我们values 里面都用来占位符最后把所有的参数传进去用executeUpdate(sql.str(), params)更新数据表int BaseDAO::insert(const BaseDO bd) { // 如果主键为空 if (!bd.getPrimaryField()-get()) throw std::runtime_error(Primary field is null.); SqlParams params; std::ostringstream col; std::ostringstream val; val ) VALUES (; // 主键字段 col bd.getPrimaryField()-getColumn(); val ?; extractFieldValue(bd.getPrimaryField(), params); // 普通字段 for (auto field : bd.getFields()) { if (extractFieldValue(field, params)) { col , field-getColumn(); val ,?; } } // 构造SQL std::ostringstream sql; sql INSERT INTO bd.getTable() (; sql col.str() val.str() ); return sqlSession-executeUpdate(sql.str(), params); }这个是更新的sqlUPDATE table SET number 123, name zhangsan WHERE id 1】里面的一个细节就是我们都是 ‘col ’一起出现的但是第一个没有‘’所以要分开讨论int BaseDAO::update(const BaseDO bd) { // 如果主键为空 if (!bd.getPrimaryField()-get()) throw std::runtime_error(Primary field is null.); SqlParams params; std::ostringstream sql; sql UPDATE bd.getTable() SET ; // 处理更新数据部分 bool isFirst true; for (auto field : bd.getFields()) { if (extractFieldValue(field, params)) { if (!isFirst) sql , ; else isFirst false; sql field-getColumn() ?; } } // 构建更新条件 extractFieldValue(bd.getPrimaryField(), params); sql WHERE bd.getPrimaryField()-getColumn() ?; // 执行更新 return sqlSession-executeUpdate(sql.str(), params); }这个相比于上个update来说唯一的区别就是传递的参数是where所以直接可以把where参数写入sql里面int BaseDAO::update(const BaseDO bd, const std::string where) { SqlParams params; std::ostringstream sql; sql UPDATE bd.getTable() SET ; // 构建更新数据部分 bool isFirst true; for (auto field : bd.getFields()) { if (extractFieldValue(field, params)) { if (!isFirst) sql , ; else isFirst false; sql field-getColumn() ?; } } // 拼接更新条件 sql WHERE where; // 执行更新 return sqlSession-executeUpdate(sql.str(), params); }这个是批量保存数据因为传递的参数是链表所以用for把所有数据一一解析先变成do然后初始化一些默认数据最后批量的插入调用dao。bool SampleService::saveDataBatch(const oatpp::ListSampleAddDTO::Wrapper datas) { UuidFacade uf(1); std::string ct SimpleDateTimeFormat::format(); std::listSampleDO dos; for (auto dto : *datas.get()) { // 组装DO数据 SampleDO data dos.emplace_back(); ZO_STAR_DOMAIN_DTO_TO_DO(data, dto, Name, name, Sex, sex, Age, age); // 生成ID data.setId(uf.genUuid()); // 设置创建人 data.setCreateBy(dto-getPayload()-getUsername()); // 设置创建时间 data.setCreateTime(ct); } // 执行数据添加 return SampleDAO().insertBatch(dos) ! 0; }更新数据和插入数据其实也是差不多的把更新的数据从dto转化成do然后设置一些默认的数据最后调用dao更新数据。移除数据就比较特别了因为删除是直接和前端对接的不需要dto和do之间的转化直接把要删除的数据传过来然后调用dao就可以删除了。我们先用std::liststd::string datas把前端传递过来的数据接受一下然后用for全部解析出对应的id因为id是每个数据的唯一标识也就是主键然后调用dao把装有id的数组传入删除。bool SampleService::updateData(const SampleDTO::Wrapper dto) { // 组装DO数据 SampleDO data; ZO_STAR_DOMAIN_DTO_TO_DO(data, dto, Name, name, Sex, sex, Age, age, Id, id); // 设置修改人 data.setUpdateBy(dto-getPayload()-getUsername()); // 设置修改时间 data.setUpdateTime(SimpleDateTimeFormat::format()); // 执行数据修改 SampleDAO dao; return dao.update(data) 1; } bool SampleService::removeData(const oatpp::Listoatpp::String ids) { SampleDAO dao; std::liststd::string datas; for (auto one : *ids.get()) datas.emplace_back(one.getValue({})); int rows dao.deleteByIdsSampleDO(datas); if (rows ids-size()) return true; return false; }controller最后我们无论怎么样都要把数据传递给前端所以还是要经过与前端打交道的controller层这一层以JsonVO的形式返回。我们controller层不参与任何数据的转化只负责调用函数。所以我们在controller层先调用service层的函数把query传递给serviceservice里面就会调用dao处理query处理完之后返回的是do这个do返回到了service然后service转化成了dto给到了result然后我们创建一个jvojvo说是传递给前端其实就是包装一层数据这一层数据包括codemessage所以我们要说明这个是success不过并不是json格式依然还是c的数据格式因为我们这个转化成json格式的任务是oat帮我们处理的SamplePageJsonVO::Wrapper SampleController::execQuerySample(const SampleQuery::Wrapper query) { // 查询数据 auto result SampleService().listAll(query); // 响应结果 auto jvo SamplePageJsonVO::createShared(); jvo-success(result); return jvo; }这个跟上一个思路基本一样大家细品~~~SampleJsonVO::Wrapper SampleController::execQueryById(const String id) { // 定义返回数据对象 auto jvo SampleJsonVO::createShared(); // 参数校验 // 非空校验 if (!id) { jvo-init(nullptr, RS_PARAMS_INVALID); return jvo; } // 执行数据新增 auto res SampleService().getById(id.getValue({})); jvo-success(res); //响应结果 return jvo; }总结本篇文章到这里就结束了这也是oatpp的第三篇文章希望可以帮助大家更好的理解~~~

相关新闻

最新新闻

日新闻

周新闻

月新闻