9. 数据库设计与使用

大家好,我是程序喵。


上一节介绍了上报协议的设计,这一节我会主要介绍埋点项目中,数据库的结构设计与使用。


提到数据库,大家肯定会想到MySQL、Postgresql,它们确实好用,不过对于客户端来说有些重。


一般客户端都是使用sqlite或者leveldb这种轻量数据库,sqlite会更常用一些。我们这里也会使用sqlite数据库。


正常在C++中使用sqlite数据库,比如增删改查,都需要写对应的数据库sql语句,相当麻烦。有其他编程经验的朋友可能会想到,有没有ORM相关的封装呢?类似于Rust中的Diesel。


C++中ORM相关的sqlite三方库很少,我找了好久,只找到了一个相对好用而且star也比较多的三方库 sqlite_orm( https://github.com/fnc12/sqlite_orm


sqlite_orm的使用非常简单,它只有一个头文件,我们只需include一个头文件就可以:https://github.com/fnc12/sqlite_orm/blob/master/include/sqlite_orm/sqlite_orm.h


写代码之前,我们需要先设计数据库的结构。


数据库中需要存储哪些内容?


其实只需要四个字段:


  1. ID:每条数据都有个对应的ID,作为数据的唯一标识,这个应该不用多说
  2. priority:表示每条数据的优先级,从数据库中取数据的时候,会按优先级从高到低读取
  3. timestamp:表示每条数据上报的时间戳
  4. content:表示实际要上传的文本内容,不过我们这里数据库中存储的数据肯定是加密过的数据,取出来需要解密后再上传到后端。否则数据都被用户看见了,不安全。


我们数据库相关的需求应该很明确,就是增删查,那相关的类如何设计呢?


// database.h

class BuriedDbImpl;
class BuriedDb {
public:
struct Data {
int32_t id;
int32_t priority;
uint64_t timestamp;
std::vector<char> content;
};

public:
BuriedDb(std::string db_path);

~BuriedDb();

void InsertData(const Data& data);

void DeleteData(const Data& data);

void DeleteDatas(const std::vector<Data>& datas);

std::vector<Data> QueryData(int32_t limit);

private:
std::unique_ptr<BuriedDbImpl> impl_;
};


对应的类叫BuriedDb,里面有几个成员函数,可以直观的看到,功能就是增删查。


类中有一个Data的struct定义,就是我们上面介绍的数据库结构需要的字段,其中有一点不太一样,正常来说content一般是string类型,而这里我们使用的是vector<char>类型,因为sqlite_orm这个库中文本类型需要是vector<char>,否则会导致数据库功能异常。


注意我们这里只使用了一个成员变量,即 std::unique_ptr<BuriedDbImpl> impl,而且这个BuriedDbImpl只有前置声明,没有具体的定义,这里使用的是C++中比较常见的pimpl模式,即头文件中不做具体定义和实现,只保留一个指针,具体的定义和实现放在源文件中。关于pimpl,我写过一篇文章,可以看看:https://t.zsxq.com/14r7qn8UQ


如果不用这种方式,我们就需要在头文件database.h中引入sqlite_orm.h,如果其他好多头文件都引入database.h,会导致重复引入sqlite_orm.h,可能会导致代码段体积过大。


再看源文件中BuriedDb成员函数的实现:


BuriedDb::BuriedDb(std::string db_path)
: impl_{std::make_unique<BuriedDbImpl>(std::move(db_path))} {}

BuriedDb::~BuriedDb() {}

void BuriedDb::InsertData(const Data& data) { impl_->InsertData(data); }

void BuriedDb::DeleteData(const Data& data) { impl_->DeleteData(data); }

void BuriedDb::DeleteDatas(const std::vector<Data>& datas) {
impl_->DeleteDatas(datas);
}

std::vector<BuriedDb::Data> BuriedDb::QueryData(int32_t limit) {
return impl_->QueryData(limit);
}


BuriedDb的所有成员函数的实现,都是调用的impl的对应方法,实际的定义都在BuriedDbImpl中。


class BuriedDbImpl {
public:
using DBStorage = decltype(InitStorage(""));

public:
BuriedDbImpl(std::string db_path) : db_path_(db_path) {
storage_ = std::make_unique<DBStorage>(InitStorage(db_path_));
storage_->sync_schema();
}

~BuriedDbImpl() {}

void InsertData(const BuriedDb::Data& data) {
auto guard = storage_->transaction_guard();
storage_->insert(data);
guard.commit();
}

void DeleteData(const BuriedDb::Data& data) {
auto guard = storage_->transaction_guard();
storage_->remove_all<BuriedDb::Data>(
where(c(&BuriedDb::Data::id) == data.id));
guard.commit();
}

void DeleteDatas(const std::vector<BuriedDb::Data>& datas) {
auto guard = storage_->transaction_guard();
for (const auto& data : datas) {
storage_->remove_all<BuriedDb::Data>(
where(c(&BuriedDb::Data::id) == data.id));
}
guard.commit();
}

std::vector<BuriedDb::Data> QueryData(int32_t limit_size) {
auto limited = storage_->get_all<BuriedDb::Data>(
order_by(&BuriedDb::Data::priority).desc(), limit(limit_size));
return limited;
}

private:
std::string db_path_;

std::unique_ptr<DBStorage> storage_;
};


这里的增删查操作都是使用的storage相关方法。


比如插入方法的三个步骤:


void InsertData(const BuriedDb::Data& data) {
auto guard = storage_->transaction_guard();
storage_->insert(data);
guard.commit();
}


先开启事务,再插入数据,再提交事务,整个插入流程非常简单而且方便,这也正是ORM数据库的优势。


代码中使用了transaction_guard开启事务,又创建了一个guard对象,并在最后使用guard.commit提交事务,这里使用了RAII概念,假如中间有哪个步骤失败,return后,会在guard的析构函数中触发回滚事务的操作。


删除查询操作,也是类似,这里就不多介绍,大家直接看代码应该就可以明白。


这里重点在介绍下storage成员变量的类型,相关代码是:


inline auto InitStorage(const std::string& path) {
return make_storage(
path, make_table("buried_data",
make_column("id", &BuriedDb::Data::id,
primary_key().autoincrement()),
make_column("priority", &BuriedDb::Data::priority),
make_column("timestamp", &BuriedDb::Data::timestamp),
make_column("content", &BuriedDb::Data::content)));
}

using DBStorage = decltype(InitStorage(""));

std::unique_ptr<DBStorage> storage_;


首先,在InitStorage函数中,使用make_storage函数创建了一个数据库存储对象。在这个函数中,使用make_table函数定义了一个名为"buried_data"的表,其中包含了"ID"、"priority"、"timestamp"和"content"四个字段。每个字段都使用了make_column函数进行定义,指定了字段的名称以及对应的数据类型。其中,"id"字段被定义为主键并自增。


接着,使用using关键字定义了一个名为DBStorage的类型,它是通过调用InitStorage函数的返回类型推断得到的。


如果不建表的话,我们还不知道storage的类型,代码中使用InitStorage函数建表,返回值就是对象类型,我们也不知道具体类型是什么,如果自己推断,那是相当麻烦。所以这里使用了inline auto + decltype关键字来推断出整个storage的类型。


最后,使用std::unique_ptr定义了一个名为storage_的成员变量,它是DBStorage类型的指针,用于操作数据库存储对象。


到这里,数据库相关的使用已经介绍完毕,下一章节我会介绍如何发起http请求,下节见。


本节相关代码详见:https://github.com/chengxumiaodaren/BuriedPoint/blob/main/src/database/database.cc


0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
程序喵
下载 APP