引言
本篇文章将重点不再放在oat++的框架上,而是用实际的例子讲述如何用框架构建一个后端交互的一个模型。
基本概念
do:这是一个我们用代码写的一个数据库,我们mysql里面存的字段在这个文件里面就会被转化成一个类对象,所以这个类对象必须要包含所有mysql的字段。
dao:data access object 这个类我们要配合mapper来使用,首先dao里面定义的是几个封装的函数,这些函数就是用来和数据库来打交道的,比如要访问数据库,新增数据啥的,所有的sql语句都是在dao里面的
service:负责和业务打交道,也就是当前端数据传递给controller之后,controller会转换成DTO,然后调用service,这一层就会把DTO变成DO,然后调用DAO来操作数据库。
mapper:这个是和dao一起的,主要是映射功能,把前端的信息映射成一个对象
代码
DO
我们还是先从DO开始说起,DO的代码只有一件事情,就是和数据库打交道,数据库里面的所有字段都要被存储下来。我们为了代码的书写方便先设置了一个宏来方便获取对应字段的一个指针,然后就是要添加对应表的所有字段。首先我们这里因为不可以有字段的重复,所以在添加的时候一个是判断是不是主键,一个是判断是不是又重复,如果有,那么就删除了再添加。
cpp
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语句的地方还是dao
cpp
class DOField final {
public:
// 字段值获取函数
using ValueGetter = std::function<void* ()>;
// 构造函数
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();
// 获取字段值
template<typename T>
const T* getValue();
private:
// 字段名称
std::string column;
// 字段类型占位符,如:s、i、b、d、dt
std::string type;
// 字段是否是主键
bool isPrimaryKey;
// 字段是否允许为空
bool isNullable;
// 获取字段值的函数
ValueGetter getter;
};
这就是最后完整的do了,其实就是一个c++型的数据库
cpp
#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_ptr<SampleDO> PtrSampleDO;
DAO
dao的作用就是用sql语句和数据库进行交互,首先我们会动态的生成一个查询器,因为sql语句语法的限制,我们必须要在WHERE 后面用AND,所以这里我们统一生成一些查询的匹配条件,比如id,name啥的,然后就是一些封装的函数,就是我们查询的方式。这里所有的查询参数都是query里面已经定义好的。也就是我上一篇文章里面的代码
cpp
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::list<SampleDO> selectWithPage(const SampleQuery::Wrapper& query);
// 通过姓名查询数据
std::list<SampleDO> selectByName(const string& name);
// 通过ID查询数据
PtrSampleDO selectById(std::string id);
};
不过这里有一个问题,就是我们查询了这么多的数据之后,我们要返回一个DO的对像。但是如果我们无法直接利用我们刚刚写的DO,因为那里面的数据太多了,我们查询的仅仅是其中的几个数据而已,所以我们就有了mapper,这个类继承了DO,首先里面专门创建了一个data,这个data里面只有我们需要查询的字段,也就是select后面需要写入的字段,作用就是我们执行查询的sql命令之后要以DO的形式返回,然后在service里面把DO转化成DTO,DTO再变成VO传递给前端
cpp
class InventoryMaterialMapper : public Mapper<InventoryMaterialDO> {
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参数其实是一个数组,这个数组里面存了两个元素,一个是占位符,因为我们传递的数据类型不同,还有一个是值。
cpp
class SqlParam
{
public:
SqlParam(std::string key, shared_ptr<void> value) {
this->key = key;
this->value = value;
}
// 数据类型占位符
std::string key;
// 对应的数据
shared_ptr<void> value;
};
// 定义一个SQL填充参数数据集类型
typedef std::vector<SqlParam> SqlParams;
cpp
#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实际上是一个数组,我们把查询的参数全部都推进了这个数组之中。
cpp
std::string SampleDAO::queryConditionBuilder(const SampleQuery::Wrapper& query, SqlParams& params)
{
stringstream sqlCondition;
sqlCondition << " WHERE 1=1";
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。
cpp
std::list<SampleDO> 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->executeQuery<SampleDO>(sql, SampleMapper(), params);
}
std::list<SampleDO> SampleDAO::selectByName(const string& name)
{
string sql = "SELECT id,name,sex,age FROM sample WHERE `name` LIKE CONCAT('%',?,'%')";
return sqlSession->executeQuery<SampleDO>(sql, SampleMapper(), "%s", name);
}
PtrSampleDO SampleDAO::selectById(std::string id)
{
string sql = "SELECT id,name,sex,age FROM sample WHERE `id`=?";
return sqlSession->executeQueryOne<PtrSampleDO>(sql, PtrSampleMapper(), "%s", id);
}
当然,我们查询数据库大多情况下需要连表查询,所以我这里的建议是再建立一个统一的mapper,里面包含了所有的表,最后返回这个mapper即可
下面是一个增加的函数案例:(也就是dao这里就是sql的语句来和数据库打交道,增删改查)
cpp
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部分,这一部分串联了各种数据的转化。
cpp
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::List<SampleAddDTO::Wrapper>& datas);
// 修改数据
bool updateData(const SampleDTO::Wrapper& dto);
// 通过ID删除数据
bool removeData(const oatpp::List<oatpp::String>& ids);
};
这个是页查询的函数,返回的也是页DTO,可能大家会认为DTO和query内容都是差不多的,但是其实有很大的区别,首先DTO的数据一般会比query要多,因为DTO是传输数据用的,说实话,query也可以拿DTO来查询,但是DTO的数据会比较杂。所以我们任何传输数据都是用DTO,如果有查询的就用query,比如判断是不是查询了id,name什么的。
首先我们要创建一个DTO的页对象,并初始化一些值,然后我们根据查询的信息拿到对应的DO数据(用dao,调用查询的函数,返回DO对象),但是我们传递的时候统统用DTO,所以最后我们一个一个的全部转化成DTO,然后把我们刚刚生成的一个对象pages赋值进去。
cpp
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();
list<SampleDO> 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用来传递
cpp
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来插入。
cpp
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)更新数据表
cpp
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(), ¶ms);
// 普通字段
for (auto field : bd.getFields())
{
if (extractFieldValue(field, ¶ms))
{
col << " ," << field->getColumn();
val << " ,?";
}
}
// 构造SQL
std::ostringstream sql;
sql << "INSERT INTO " << bd.getTable() << " (";
sql << col.str() << val.str() << " )";
return sqlSession->executeUpdate(sql.str(), params);
}
这个是更新的sql:
UPDATE table SET number = 123, name = zhangsan WHERE id = 1】
里面的一个细节就是我们都是 ',col = ?'一起出现的但是第一个没有',',所以要分开讨论
cpp
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, ¶ms))
{
if (!isFirst) sql << ", ";
else isFirst = false;
sql << field->getColumn() << " = ?";
}
}
// 构建更新条件
extractFieldValue(bd.getPrimaryField(), ¶ms);
sql << " WHERE " << bd.getPrimaryField()->getColumn() << " = ?";
// 执行更新
return sqlSession->executeUpdate(sql.str(), params);
}
这个相比于上个update来说唯一的区别就是传递的参数是where,所以直接可以把where参数写入sql里面
cpp
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, ¶ms))
{
if (!isFirst) sql << ", ";
else isFirst = false;
sql << field->getColumn() << " = ?";
}
}
// 拼接更新条件
sql << " WHERE " << where;
// 执行更新
return sqlSession->executeUpdate(sql.str(), params);
}
这个是批量保存数据,因为传递的参数是链表,所以用for()把所有数据一一解析,先变成do,然后初始化一些默认数据,最后批量的插入调用dao。
cpp
bool SampleService::saveDataBatch(const oatpp::List<SampleAddDTO::Wrapper>& datas)
{
UuidFacade uf(1);
std::string ct = SimpleDateTimeFormat::format();
std::list<SampleDO> 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::list<std::string> datas把前端传递过来的数据接受一下,然后用for()全部解析出对应的id,因为id是每个数据的唯一标识也就是主键,然后调用dao把装有id的数组传入删除。
cpp
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::List<oatpp::String>& ids)
{
SampleDAO dao;
std::list<std::string> datas;
for (auto one : *ids.get())
datas.emplace_back(one.getValue({}));
int rows = dao.deleteByIds<SampleDO>(datas);
if (rows == ids->size())
return true;
return false;
}
controller
最后我们无论怎么样都要把数据传递给前端,所以还是要经过与前端打交道的controller层,这一层以JsonVO的形式返回。我们controller层不参与任何数据的转化,只负责调用函数。
所以我们在controller层先调用service层的函数,把query传递给service,service里面就会调用dao处理query,处理完之后返回的是do,这个do返回到了service,然后service转化成了dto,给到了result,然后我们创建一个jvo,jvo说是传递给前端,其实就是包装一层数据,这一层数据包括code,message,所以我们要说明这个是success,不过并不是json格式,依然还是c++的数据格式,因为我们这个转化成json格式的任务是oat++帮我们处理的
cpp
SamplePageJsonVO::Wrapper SampleController::execQuerySample(const SampleQuery::Wrapper& query)
{
// 查询数据
auto result = SampleService().listAll(query);
// 响应结果
auto jvo = SamplePageJsonVO::createShared();
jvo->success(result);
return jvo;
}
这个跟上一个思路基本一样,大家细品~~~
cpp
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的第三篇文章,希望可以帮助大家更好的理解~~~