C++11 mysql数据库从原生api的封装到ORM库的实现 [高仿通用mapper接口]

一: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

github地址----基于C++11的ORM库

2:码云

码云地址----基于C++11的ORM库
更多详情请移步代码仓库。

  • 8
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1、本课程是一个干货课程,主要讲解如何封装服务器底层,使用Tcp/ip长连接,IDE使用vs2019 c++开发以及使用c++11的一些标准,跨平台windows和linux,服务器性能高效,单服务器压力测试上万无压力,服务器框架是经历过上线产品的验证,框架简单明了,不熟悉底层封装的人,半个小时就能完全掌握服务器框架上手写业务逻辑。2、本课程是一个底层服务器框架教程,主要是教会学员在windows或linux下如何封装一个高效的,避免踩坑的商业级框架,服务器底层使用初始化即开辟内存的技术,使用内存池,服务器运行期间内存不会溢出,非常稳定,同时服务器使用自定义哈希hashContainer,在处理新的连接,新的数据,新的封包,以及解包,发包,粘包的过程,哈希容器性能非常高效,增、删、查、改永远不会随着连接人数的上升而降低性能,增、删、查、改的复杂度永远都是恒定的O(1)。3、服务器底层封装没有使用任何第三方网络以及任何第三方插件,自由度非常的高,出了任何BUG,你都有办法去修改,查找问题也非常方便,在windows下使用iocp,linux下使用epoll.4、讲解c++纯客户端,主要用于服务器之间通信,也就是说你想搭建多层结构的服务器,服务器与服务器之间使用socket通信。还可以使用c++客户端做压力测试,开辟多线程连接服务器,教程提供了压力测试,学员可以自己做压力测试服务器性能。5、赠送ue4和unity3d通信底层框架以及多人交互demo,登录,注册,玩家离开,同步主要是教会学员服务器与客户端如何交互。6、赠送c++连接mysql数据库框架demo,登录,注册,玩家离开数据持久化.7、服务器教程使用自定义通信协议,同时也支持protobuf,选择权在开发者自己手里,想用什么协议都可以,自由度高。8、服务器教程使用手动敲代码逐句讲解的方式开展教学课程。非喜勿喷,谢谢大家。9、服务器教程提供源码,大家可以在平台提供的地址下载或者联系我,服务器使用c++11部分标准,std::thread,条件变量,线程锁,智能指针等,需要学员具备一定c++知识,购买前请慎重考虑。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值