一:mysql原生api的封装和连接池的实现
ORM封装的第一步,需要对mysql原生api进行封装,让之后的调用更加便捷。同时,为了能复用连接,提高获取连接的效率,还做了个连接池。具体封装和实现可查看
二:ORM的实现
1:封装初衷
做过Java web服务器开发的都知道,Java有很多非常优秀的ORM框架,例如mybatis,也有人对mybatis进行二次封装,成就了更加方便的通用mapper。然而,如果现在你用的是C++开发,那么相对应的ORM框架是真的少之又少。
通过对比Java的ORM框架,对照通用mapper框架,实现了基于C++11的ORM,实现了Java通用mapper的接口的基本接口,也支持连表查询。
2:封装思路
- 实体类中成员与表字段关系的保存
C++并没有反射,要做到对实体类信息的保存,便需要在编译期便提前将类相关的成员的指针信息都保存起来。如何才能更加方便的保存这些信息,并且又不污染实体类本身呢?
我们可以采用模板类的方式,在定义实体类之后,同时也将实体类以一种不经意的方式包裹在模板类中。这样,我们获取类信息的时候,通过模板类进行获取。例如:
先定义模板类
EntityWrapper
/**
* 实体类的外包增强类
*/
template<typename Entity>
class EntityWrapper {
public:
/**
* 获取实体类的反射信息
* @param entity
*/
void *getReflectionInfo(Entity *entity) {
return nullptr;
}
};
这是普通的实体类
struct Student {
int id = 0;
std::string name;
int classId = 0;
std::time_t createTime;
};
使用的时候,结合元组std::tuple,有了C++11的类型自动推断,便能将成员指针信息,数据库字段等信息保存起来,当然,之后要拿出来用的时候,便是对元组的操作了。
template<>
class EntityWrapper<Student> {
public:
auto getReflectionInfo(Student *entity) {
return std::make_tuple(
EntityTable("Student","student"),
std::make_pair(&Student::id,EntityColumn(entity, &entity->id, "id"...)),
std::make_pair(&Student::name,EntityColumn(entity, &entity->name, "id"...)),
std::make_pair(&Student::classId,EntityColumn(entity, &entity->classId, "class_id"...)),
std::make_pair(&Student::createTime,EntityColumn(entity, &entity->createTime, "create_time"...))
);
}
};
之后,我们便可以通过EntityWrapper< Student >类来获取Student对象的相关信息。
其中,对于成员函数指针:auto field = &Student::id,可以认为是成员在对象中的偏移地址,其数据类型为int Student:: * ,如果已经知道了一个具体的对象 Student stu,那我们便可以通过该成员指针获取到该成员的内容:stu.*field
- 实体类信息映射的获取
有了实体类反射信息的存储途径,接下来是对反射信息的获取。比如说我们想通过成员指针来获取该成员绑定的具体信息,我们就需要通过元组来进行操作了,比如说要实现如下函数:
/**
* 根据类在函数中的偏移地址获取对应的实体列属性名
* @tparam T 返回类型
* @tparam Entity 实体类
* @param t 偏移地址
* @param propertyMap Entity类某个对象的属性列
* @return
*/
template<typename T, typename Entity>
static std::string getProperty(T Entity::* t) {
auto entity = std::make_shared<Entity>();
auto resultTuple = EntityWrapper<Entity>().getReflectionInfo(entity.get());
return getProperty(resultTuple, t);
}
那么,首先便需要提取元组的信息,找到对应的信息返回。而C++对于元组信息的提取,需要利用编译期的模板递归的写法,以及模板的自动匹配特性,其中涉及到一点模板元编程的知识。部分实现如下,具体实现可参考项目代码。
template<typename Tuple, size_t N>
struct ResultGetter {
/**
* 获取与t偏移位置相同的属性名
* @tparam T
* @param tuple
* @param t
* @return
*/
template<typename T>
static void getProperty(const Tuple &tuple, T t, std::string &property) {
ResultGetter<Tuple, N - 1>::getProperty(tuple, t, property);
auto val = std::get<N - 1>(tuple);
if (EntityTupleGetter::isMatchProperty(val, t)) {
property = EntityTupleGetter::getProperty(val);
}
}
}
template<typename Tuple>
struct ResultGetter<Tuple, 0> {
template<typename T>
static void getProperty(const Tuple &, T, std::string &) {}
};
//获取属性字符串的实现
template<typename... Args, typename T>
static std::string getProperty(const std::tuple<Args...> &tuple, T t) {
std::string res;
ResultGetter<decltype(tuple), sizeof...(Args)>::getProperty(tuple, t, res);
return res;
}
- 通用对象Object和通用列表Iterable的封装
虽然C++的模板能够方便我们对数据进行操作,但是并不方便我们对数据进行保存。对于ORM框架的封装,虽然依旧可以用模板的来对不同数据类型的地址进行操作,但始终还是不方便的,而且操作mysql的预处理和结果集,是需要有数据地址作为媒介的。所以需要一个万能类型来保存对应的数据,C++17有std::any类型,boost框架也有boost::any类型。项目基于C++11,所以自己实现了个比较简单的类型Object,这里用到了模板类型的构造函数来实现对数据类型的擦除和保存。同时多开辟了一些空间来对数据进行缓存,方便后续mysql对地址的取用。
/**
* 简单包装一下Object类型,方便之后取出来使用
*/
class Object {
protected:
struct Buff {
std::vector<char> stringValue;//字符串的缓存区
int intValue = 0;//整型的缓存区
std::vector<Object> values;
} buff;//缓存数据的内容
std::type_index typeIndex = std::type_index(typeid(void));//存放的是值的类型
bool container = false;//是否是一个容器值
bool null = true;//是不是空的
protected:
//专门供给子类调用的构造函数
Object(std::type_index typeIndex, bool container, bool null)
: typeIndex(typeIndex), container(container), null(null) {}
public:
//创建一个相对应类型的Object,构造函数的形式
Object(const std::type_index &typeIndex) : typeIndex(typeIndex) {}
//将可以转为std::string类型,都归入std::string类型
Object(const std::string &value) : typeIndex(typeid(std::string)), null(false) {
buff.stringValue.assign(value.begin(), value.end());
buff.stringValue.emplace_back('\0');//必须加入结束符,避免之后不必要的错误
};
//将可以转为const char*类型,也都归入std::string类型
Object(const char *value) : typeIndex(typeid(std::string)), null(false) {
buff.stringValue.resize(strlen(value));
std::memcpy(buff.stringValue.data(), value, strlen(value));
buff.stringValue.emplace_back('\0');//必须加入结束符,避免之后不必要的错误
};
//将可以转为int类型,都归入int类型
Object(int value) : typeIndex(typeid(int)), null(false) { buff.intValue = value; };
Object() = default;
};
同时也需要支持一般化C++容器,为此提供可迭代类Iterable
//包装了集合类
class Iterable : public Object {
private:
/**
* 将容器的值保存到std::vector<Object>中存储
* @tparam Collection
* @param collection
* @return
*/
template<typename Collection>
void save2Collection(const Collection &collection) {
for (const auto &c:collection) {
//加入values集合中,注意,不能push_back,避免对象发生赋值
this->buff.values.emplace_back(c);
}
}
public:
template<typename T>
Iterable(const std::set<T> &value) : Object(typeid(std::set<T>), true, false) { save2Collection(value); };
template<typename T>
Iterable(const std::list<T> &value) : Object(typeid(std::list<T>), true, false) { save2Collection(value); };
template<typename T>
Iterable(const std::vector<T> &value) : Object(typeid(std::list<T>), true, false) { save2Collection(value); };
template<typename T>
Iterable(const std::initializer_list<T> &value) : Object(typeid(std::initializer_list<T>), true, false) {
save2Collection(value);
};
Iterable() = default;
int size() const {
return this->buff.values.size();
}
const Object &operator[](int index) const {
if (index > size()) {
std::cerr << "[out of max index]" << std::endl;
throw MapperException("[out of max index]");
}
return this->buff.values[index];
}
};
这个做法有比较大的缺点,就是该Object对象的空间比较大,有比较多空间的浪费,后续有时间会参考 std::any 和 boost::any 的做法进行改进。
- 数据库查询语句构建器的封装
获取到了实体类的反射信息,便可以通过实体类绑定的信息来找到对应的数据库的字段信息,然后拼接对应的SQL语句,再绑定预处理的参数值,便能做到自动查出结果来,然后将查询的结果绑定到先前最开始保存再元组中的对象成员地址空间里,便能实现自动映射到实体类中。
为了拼接SQL语句的方便,还仿造了mybatis的SQL语句构建器,实现了基于C++的SQLBuilder。
C++ SQL语句构建器的实现[与mybatis3使用方式一致]
3:测试结果
项目ORM使用起来也挺方便的
- 表格的准备
CREATE TABLE `t_school` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8
- 实体类的准备
需要编写实体类与数据库表字段的映射关系。如果不指定数据库对应的名称,默认采用实体类驼峰命名法到下划线命名法的转换。
struct School {
int id = 0;
std::string name;
std::time_t createTime = {};
School() = default;
School(const std::string &name, time_t createTime) : name(name), createTime(createTime) {}
School(int id, const std::string &name, time_t createTime) : id(id), name(name), createTime(createTime) {}
friend std::ostream &operator<<(std::ostream &os, const School &school) {
os << "id: " << school.id << " name: " << school.name << " createTime: " << school.createTime;
return os;
}
};
ResultMap(
EntityMap(School, "t_school"),
PropertyMap(id, ColumnType::Id),
PropertyMap(name),
PropertyMap(createTime)
)
- 单表查询
然后就可以进行数据的一系列查询操作了
插入操作
Mapper<School> schoolMapper;
std::cout << "==================插入三个学校==================" << std::endl;
std::cout << "【学校insertId】"<< schoolMapper.insert(School("Hello-School", system_clock::to_time_t(system_clock::now())))<< std::endl;
std::cout << "【学校insertId】" << schoolMapper.insert(School("World-School",system_clock::to_time_t(system_clock::now()))) << std::endl;
std::cout << "【学校insertId】" << schoolMapper.insert(School("My-School",system_clock::to_time_t(system_clock::now()))) << std::endl;
输出结果为
==================插入三个学校==================
【学校insertId】7
【学校insertId】8
【学校insertId】9
查询操作
std::cout << "==================查询所有学校==================" << std::endl;
for (auto &school:schoolMapper.selectAll()) {
std::cout << school << std::endl;
}
输出结果为
==================查询所有学校==================
id: 7 name: Hello-School createTime: 1590210918
id: 8 name: World-School createTime: 1590210918
id: 9 name: My-School createTime: 1590210918
更新操作
std::cout << "==================更新学校==================" << std::endl;
std::cout << "【受影响行数】"<< schoolMapper.updateByPrimaryKey(School(7,"Hello-School_update", system_clock::to_time_t(system_clock::now())))<< std::endl;
输出结果为
==================更新学校==================
【受影响行数】1
删除操作
std::cout << "==================删除学校==================" << std::endl;
std::cout << "【受影响行数】"<< schoolMapper.deleteByPrimaryKey(7)<< std::endl;
输出结果为
==================删除学校==================
【受影响行数】1
复杂的查询,Example的使用
现在表中的数据如下
id name create_time
8 World-School 2020-05-23 13:15:18
9 My-School 2020-05-23 13:15:18
10 Hello-School_update 2020-05-23 13:26:17
查询name中含有“d”并且id大于8的数据
std::cout << "==================查询name中含有“d”并且id大于8的数据==================" << std::endl;
//构造Example,构造查询条件
Example<School> example;
auto criteria= example.createCriteria();
criteria->andLike(&School::name,"%d%");
criteria->andGreaterThan(&School::id,8);
//执行查询
for (auto &school:schoolMapper.selectByExample(example)) {
std::cout << school << std::endl;
}
输出结果为
==================查询name中含有“d”并且id大于8的数据==================
id: 10 name: Hello-School_update createTime: 1590211577
其中,Example< T >和 Criteria 的更多用法参照Java的通用mapper
- 连表查询
该ORM除了支持单表查询,还支持连表查询。
一对一连表查询
表信息如下:
-- 班级表
CREATE TABLE `class` (
`class_id` int NOT NULL AUTO_INCREMENT,
`class_name` varchar(255) DEFAULT NULL,
`school_id` int DEFAULT NULL,
PRIMARY KEY (`class_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8
-- 学生表
CREATE TABLE `student` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`class_id` int DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
表数据如下
-- class表
class_id class_name school_id
1 classA 8
2 classB 8
3 classC 9
-- student表
id name class_id create_time
1 zhangsan 1 2020-05-23 13:56:51
2 lisi 1 2020-05-23 13:57:05
3 wangwu 2 2020-05-23 13:57:24
4 zhaoliu 3 2020-05-23 13:57:39
实体类与表字段的映射信息
struct Class {
int classId = 0;
std::string className;
int schoolId = 0;
friend std::ostream &operator<<(std::ostream &os, const Class &aClass) {
os << "classId: " << aClass.classId << " className: " << aClass.className << " schoolId: " << aClass.schoolId;
return os;
}
};
ResultMap(
EntityMap(Class),
PropertyMap(classId, ColumnType::Id),
PropertyMap(className),
PropertyMap(schoolId)
)
struct Student {
int id = 0;
std::string name;
Class clazz;
std::time_t createTime;
friend std::ostream &operator<<(std::ostream &os, const Student &student) {
os << "id: " << student.id << " name: " << student.name << " clazz: " << student.clazz << " createTime: "
<< student.createTime;
return os;
}
};
ResultMap(
EntityMap(Student),
PropertyMap(id, ColumnType::Id),
PropertyMap(name),
PropertyMap(clazz, "class_id", JoinType::OneToOne, &Class::classId),
PropertyMap(createTime)
)
查询学生id大于1并且班级名称是“classA”的学生
std::cout << "==================查询学生id大于1并且班级名称是“classA”的学生==================" << std::endl;
Mapper <Student> studentMapper;
Example <Student> example;
auto criteria = example.createCriteria();
criteria->andGreaterThan(&Student::id, 1);
criteria->andEqualTo(&Class::className, "classA");
for (auto &school:studentMapper.selectByExample(example)) {
std::cout << school << std::endl;
}
输出结果为
==================查询学生id大于1并且班级名称是“classA”的学生==================
id: 2 name: lisi clazz: classId: 1 className: classA schoolId: 8 createTime: 1590213425
一对多连表查询
该ORM关系同时支持一对多的连表查询
将上面School类的信息改一改,改为如下结构
struct School {
int id = 0;
std::string name;
std::time_t createTime = {};
std::vector<Class> clazzs;
friend std::ostream &operator<<(std::ostream &os, const School &school) {
os << "id: " << school.id << " name: " << school.name << " createTime: " << school.createTime;
os << " clazzs: [";
for (int i = 0; i < school.clazzs.size(); i++) {
os << " { ";
os << school.clazzs[i];
os << "} ";
}
os << "]";
return os;
}
};
ResultMap(
EntityMap(School, "t_school"),
PropertyMap(id, ColumnType::Id),
PropertyMap(name),
PropertyMap(createTime),
//特别注意,“id”为School表的连接字段id,&Class::schoolId为class表的连接字段
PropertyMap(clazzs, "id", JoinType::OneToMany, &Class::schoolId)
)
依旧还是之前的查询接口
//连表查询
Mapper<School> schoolMapper;
std::cout << "==================查询所有学校==================" << std::endl;
for (auto &school:schoolMapper.selectAll()) {
std::cout << school << std::endl;
}
查询结果如下
==================查询所有学校==================
id: 8 name: World-School createTime: 1590210918 clazzs: [ { classId: 2 className: classB schoolId: 8} { classId: 1 className: classA schoolId: 8} ]
id: 9 name: My-School createTime: 1590210918 clazzs: [ { classId: 3 className: classC schoolId: 9} ]
id: 10 name: Hello-School_update createTime: 1590211577 clazzs: []
三:总结
这个项目广泛运用了模板的特性,元组等操作,结合一些Java的ORM使用经验,使用方便,也不需要用到更高级的C++特性。
四:代码仓库
1:github
2:码云
码云地址----基于C++11的ORM库
更多详情请移步代码仓库。