【在线五子棋对战】七、数据管理模块实现
文章目录
- 前言
- Ⅰ. 数据库表的设计
- Ⅱ. 数据管理模块的封装和实现
- 构造函数
- 析构函数
- sign_up注册函数
- login登录函数
- select_by_name获取信息函数
- select_by_id获取信息函数
- win胜利处理函数
- lose失败处理函数
- 测试代码
- 完整代码

前言
数据管理模块主要负责对于数据库中数据进行统一的增删改查管理,其他模块要对数据操作都必须通过数据管理模块完成。主要分为下面的两大块进行设计:
- 数据库表的设计
- 数据管理模块的封装和实现
Ⅰ. 数据库表的设计
创建一个 user 表, 用来表示用户信息及积分信息:
- 用户信息:用来实现登录、注册、游戏对战数据管理等功能
- 积分信息:用来实现匹配功能
也就是说我们的表里面要有下面这些字段:
- id:作为主键,用于表示用户的唯一性
- username:用户名称
- password:密码
- score:用户的积分
- total_count:用户总的比赛场次
- win_count:用户总的胜利比赛场次
所以我们创建一个 .sql
文件,创建一个数据库,叫做 gobang
,然后创建一张表,叫做 user
,然后将这些字段放到 user
表中!
create database if not exists gobang;
use gobang;
create table if not exists user(id int primary key auto_increment,username varchar(32) unique key not null,password varchar(128) not null,score int,total_count int,win_count int
);
如果在测试代码的时候需要删掉该数据库中表,那么可以在上述代码最上方加入此行 mysql 指令:
drop database if exists gobang;
然后重新将
db.sql
导入到客户端中即可!
其中 username
我们设为具有唯一性的!
接下来我们保存这个文件内容,然后将该文件内容重定向到 mysql -uroot
指令中去,也就是如下操作:
[root@VM-8-7-centos source]# mysql -uroot < db.sql
[root@VM-8-7-centos source]# mysql -uroot
……
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| gobang |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.00 sec)mysql> use gobang;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -ADatabase changed
mysql> show tables;
+------------------+
| Tables_in_gobang |
+------------------+
| stu |
| user |
+------------------+
2 rows in set (0.00 sec)mysql> desc user;
+-------------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| username | varchar(32) | NO | UNI | NULL | |
| password | varchar(128)| NO | | NULL | |
| score | int(11) | YES | | NULL | |
| total_count | int(11) | YES | | NULL | |
| win_count | int(11) | YES | | NULL | |
+-------------+-------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)
Ⅱ. 数据管理模块的封装和实现
数据库中有可能存在很多张表,每张表中管理的数据又有不同,要进行的数据操作也各不相同,因此我们可以为每⼀张表中的数据操作都设计一个类,通过类实例化的对象来管理这张数据库表中的数据,这样的话当我们要访问哪张表的时候,使用哪个类实例化的对象即可。
这里创建一个 user_table
类, 该类的作用是负责通过 MySQL
接⼝管理用户数据。主要提供以下几种方法:
- sign_up():新增用户,实现注册功能
- login():登录验证,并获取完整的用户信息
- select_by_name():根据用户名查找用户信息
- select_by_id():根据 id 查找用户信息
- win():胜利时天梯分数增加,战斗场次增加,胜利场次增加
- lose():失败时天梯分数减少,战斗场次增加,其它不变
除此之外,因为我们要操作这个数据库表,所以我们**得有 MYSQL* 操作句柄,另外因为有可能两个线程同时在访问或者修改数据库表,那么就会导致数据出现错误,所以必须加锁**!
所以大体的类框架如下:
#ifndef __MY_DB_H__
#define __MY_DB_H__
#include "util.hpp"
#include <mutex>class user_table
{
private:MYSQL* _mysql; // 操作句柄std::mutex _mtx; // 互斥锁保护数据库的操作
public:user_table(){}~user_table(){}// 注册函数bool sign_up(Json::Value& user){}// 登录函数bool login(Json::Value& user){}// 通过用户名获取用户信息bool select_by_name(const std::string& name, Json::Value& user){}// 通过id获取用户信息bool select_by_id(uint64_t id, Json::Value& user){}// 胜利处理函数bool win(uint64_t id){}// 失败处理函数bool lose(uint64_t id){}
};#endif
构造函数
构造函数就是对操作句柄的初始化嘛!
user_table(const std::string& host,const std::string& user,const std::string& passwd,const std::string& dbname,uint16_t port = 3306)
{_mysql = mysql_util::mysql_create(host, user, passwd, dbname, port);assert(_mysql != NULL);
}
析构函数
析构函数就是释放句柄!
~user_table()
{mysql_util::mysql_destroy(_mysql);
}
sign_up注册函数
- 首先肯定是要判断用户是否提供了用户名和密码。如果连用户名和密码都没有的话,那么直接返回
false
即可 - 接着就是将我们宏定义的
sql
语句,通过sprintf
函数格式化放到字符数组query
中 - 最后就是执行
sql
语句啦,执行完判断一下是否成功即可!
bool sign_up(Json::Value& user)
{
#define SIGN_UP "insert user values(null, '%s', password('%s'), 1000, 0, 0);"// 1. 首先判断是否提供了用户名和密码 -- 并且用户名密码不能为空if(user["username"].isNull() || user["password"].isNull() || user["username"].asString().empty() || user["password"].asString().empty()){DLOG("user didn't enter an username or password!");return false;}// 2. 将语句格式化后放到query数组中char query[4096] = {0};sprintf(query, SIGN_UP, user["username"].asCString(), user["password"].asCString());// 3. 执行语句bool ret = mysql_util::mysql_exec(_mysql, query);if(ret == false){DLOG("sign up user failed!");return false;}return true;
}
login登录函数
登录函数相对前面来说就比较复杂了,因为我们要执行的是查询语句,查询语句又涉及到了保存结果集到本地,获取结果集等等的操作,那么相对来说会繁琐一些,但是步骤还是比较清晰的!下面是几大步骤:
- 首先判断是否提供了用户名和密码。如果连用户名和密码都没有的话,那么直接返回
false
即可 - 将语句格式化后放到
query
数组中 - 执行
sql
语句,并且保存结果集到本地- 为什么将这两个操作放到一步呢,因为我们要对这两个操作进行加锁,为什么要加锁呢❓❓❓
- 因为查询完之后我们下一步就是将其结果集保存到本地,有可能此时还有其它的线程在执行
sql
语句,此时要是还没保存就被修改或者删除了,就造成线程安全问题了,所以必须加锁!
- 获取结果集条数,得到结果集
- 这里行数肯定是只有一行,因为在表中我们规定了用户名是唯一的!
- 将数据库中的用户信息填写到
user
对象中去,作为输出型参数- 这里所需注意的就是类型问题,具体看代码
- 释放结果集
bool login(Json::Value& user)
{
#define LOGIN_SQL "select id, score, total_count, win_count from user where username='%s' and password=password('%s');"// 1. 首先判断是否提供了用户名和密码 -- 并且用户名密码不能为空if(user["username"].isNull() || user["password"].isNull() || user["username"].asString().empty() || user["password"].asString().empty()){DLOG("user didn't enter an username or password!");return false;}// 2. 将语句格式化后放到query数组中char query[4096] = {0};sprintf(query, LOGIN_SQL, user["username"].asCString(), user["password"].asCString());// 3. 执行sql语句,因为查询之后需要保存到本地,为了保证保存过程的线程安全,这段代码需要加锁// 这里不直接使用加锁,而是通过守卫锁来管理锁,更加安全// 并且因为守卫锁是当前作用域有效,所以只对要加锁的区域放在一个空代码块中当作一个作用域MYSQL_RES* res = nullptr;{std::unique_lock<std::mutex> lock(_mtx); // 相当于加锁了// 执行sql语句bool ret = mysql_util::mysql_exec(_mysql, query);if(ret == false){if(ret == false){DLOG("user login failed!");return false;}}// 保存查询结果到本地res = mysql_store_result(_mysql);if(res == nullptr){DLOG("mysql_store_result!");return false;}}// 4. 获取结果集条数,得到结果集 -- 这里行数肯定是只有一行,因为在表中我们规定了用户名是唯一的int row_num = mysql_num_rows(res);if(row_num == 0){DLOG("the user information is not found!");return false;}else if (row_num != 1) {DLOG("the user information queried is not unique!");return false;}MYSQL_ROW row = mysql_fetch_row(res);// 5. 将数据库中的用户信息填写到user对象中去,作为输出型参数// 这里有细节,因为结果集中的数据都是字符串,所以转化为整型,最好是长整型// 但是转化为长整型会报错,所以再强转为json的数据类型,如下面的Json::UInt64user["id"] = (Json::UInt64)std::stol(row[0]);user["score"] = (Json::UInt64)std::stol(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);// 6. 别忘了释放结果集mysql_free_result(res);return true;
}
select_by_name获取信息函数
因为涉及到的依然是查询语句,其实大体的过程和上面的登录函数是类似的,不同的就是 sql
语句改变了、一些日志内容改变、用户信息填写时候多填一个姓名的字段,仅此而已,这里就不多赘述了,具体参考上面登录函数,结合下面的代码注释:
bool select_by_name(const std::string& name, Json::Value& user)
{
#define SELECT_BY_NAME "select id, score, total_count, win_count from user where username='%s';"// 1. 将语句格式化后放到query数组中char query[4096] = {0};sprintf(query, SELECT_BY_NAME, name.c_str());// 2. 执行语句,并且保存结果集到本地 -- 因为是查询语句,所以还是依然要加锁保证线程安全MYSQL_RES* res = nullptr;{std::unique_lock<std::mutex> lock(_mtx); // 相当于加锁// 执行语句bool ret = mysql_util::mysql_exec(_mysql, query);if(ret == false){DLOG("get user by name failed!!");return false;}// 保存结果集res = mysql_store_result(_mysql);if(res == nullptr){DLOG("mysql_store_result failed");return false;}}// 3. 获取结果集条数,得到结果集int row_num = mysql_num_rows(res);if(row_num == 0){DLOG("the user information is not found!");return false;}else if(row_num != 1){DLOG("the user information queried is not unique!!");return false;}MYSQL_ROW row = mysql_fetch_row(res);// 4. 将数据库中的用户信息填写到user对象中去,作为输出型参数user["id"] = (Json::UInt64)std::stol(row[0]);user["username"] = name;user["score"] = (Json::UInt64)std::stol(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);// 5. 释放结果集mysql_free_result(res);return true;
}
select_by_id获取信息函数
上面的 select_by_name
函数修改细节就能变成这个函数!
bool select_by_id(uint64_t id, Json::Value& user)
{
#define SELECT_BY_ID "select username, score, total_count, win_count from user where id='%d';"// 1. 将语句格式化后放到query数组中char query[4096] = {0};sprintf(query, SELECT_BY_ID, id);// 2. 执行语句,并且保存结果集到本地 -- 因为是查询语句,所以还是依然要加锁保证线程安全MYSQL_RES* res = nullptr;{std::unique_lock<std::mutex> lock(_mtx); // 相当于加锁// 执行语句bool ret = mysql_util::mysql_exec(_mysql, query);if(ret == false){DLOG("get user by id failed!!");return false;}// 保存结果集res = mysql_store_result(_mysql);if(res == nullptr){DLOG("mysql_store_result failed");return false;}}// 3. 获取结果集条数,得到结果集int row_num = mysql_num_rows(res);if(row_num == 0){DLOG("the user information is not found!");return false;}else if(row_num != 1){DLOG("the user information queried is not unique!!");return false;}MYSQL_ROW row = mysql_fetch_row(res);// 4. 将数据库中的用户信息填写到user对象中去,作为输出型参数user["id"] = (Json::UInt64)id;user["username"] = row[0];user["score"] = (Json::UInt64)std::stol(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);// 5. 释放结果集mysql_free_result(res);return true;
}
win胜利处理函数
这函数比较简单了,因为用到的 sql
语句是修改语句,不需要做太多工作,具体看代码:
// 胜利处理函数 -- 胜利时天梯分数增加30分,战斗场次增加1,胜利场次增加1
bool win(uint64_t id)
{
#define WIN "update user set score=score+30, total_count=total_count+1, win_count=win_count+1 where id='%d';"// 1. 将语句格式化后放到query数组中char query[4096] = {0};sprintf(query, WIN, id);// 2. 执行语句bool ret = mysql_util::mysql_exec(_mysql, query);if(ret == false){DLOG("update win user info failed!!\n");return false;}return true;
}
lose失败处理函数
上面的 win
函数修改一下就变成了这里的函数!
// 失败处理函数 -- 失败时天梯分数减少30,战斗场次增加1,其他不变
bool lose(uint64_t id)
{
#define LOSE "update user set score=score-30, total_count=total_count+1 where id='%d';"// 1. 将语句格式化后放到query数组中char query[4096] = {0};sprintf(query, LOSE, id);// 2. 执行语句bool ret = mysql_util::mysql_exec(_mysql, query);if(ret == false){DLOG("update lose user info failed!!\n");return false;}return true;
}
测试代码
void db_test()
{user_table ut(HOST, USER, PASSWD, DBNAME, PORT);Json::Value user;// user["username"] = "xiaoming";// user["password"] = "123456";bool ret = ut.lose(1);if(ret == false){DLOG("login failed!");return;}std::string body;json_util::serialize(user, body);std::cout << body << std::endl;
}
完整代码
#ifndef __MY_DB_H__
#define __MY_DB_H__
#include "util.hpp"
#include <mutex>
#include <cassert>class user_table
{
private:MYSQL* _mysql; // 操作句柄std::mutex _mtx; // 互斥锁保护数据库的操作
public:user_table(const std::string& host,const std::string& user,const std::string& passwd,const std::string& dbname,uint16_t port = 3306){_mysql = mysql_util::mysql_create(host, user, passwd, dbname, port);assert(_mysql != NULL);}~user_table(){mysql_util::mysql_destroy(_mysql);_mysql = NULL;}// 注册函数bool sign_up(Json::Value& user){
#define SIGN_UP "insert user values(null, '%s', password('%s'), 1000, 0, 0);"// 1. 首先判断是否提供了用户名和密码 -- 并且用户名密码不能为空if(user["username"].isNull() || user["password"].isNull() || user["username"].asString().empty() || user["password"].asString().empty()){DLOG("user didn't enter an username or password!");return false;}// 2. 将语句格式化后放到query数组中char query[4096] = {0};sprintf(query, SIGN_UP, user["username"].asCString(), user["password"].asCString());// 3. 执行语句bool ret = mysql_util::mysql_exec(_mysql, query);if(ret == false){DLOG("sign up user failed!");return false;}return true;}// 登录函数bool login(Json::Value& user){
#define LOGIN_SQL "select id, score, total_count, win_count from user where username='%s' and password=password('%s');"// 1. 首先判断是否提供了用户名和密码 -- 并且用户名密码不能为空if(user["username"].isNull() || user["password"].isNull() || user["username"].asString().empty() || user["password"].asString().empty()){DLOG("user didn't enter an username or password!");return false;}// 2. 将语句格式化后放到query数组中char query[4096] = {0};sprintf(query, LOGIN_SQL, user["username"].asCString(), user["password"].asCString());// 3. 执行sql语句,因为查询之后需要保存到本地,为了保证保存过程的线程安全,这段代码需要加锁// 这里不直接使用加锁,而是通过守卫锁来管理锁,更加安全// 并且因为守卫锁是当前作用域有效,所以只对要加锁的区域放在一个空代码块中当作一个作用域MYSQL_RES* res = nullptr;{std::unique_lock<std::mutex> lock(_mtx); // 相当于加锁了// 执行sql语句bool ret = mysql_util::mysql_exec(_mysql, query);if(ret == false){if(ret == false){DLOG("user login failed!");return false;}}// 保存查询结果到本地res = mysql_store_result(_mysql);if(res == nullptr){DLOG("mysql_store_result!");return false;}}// 4. 获取结果集条数,得到结果集 -- 这里行数肯定是只有一行,因为在表中我们规定了用户名是唯一的int row_num = mysql_num_rows(res);if(row_num == 0){DLOG("the user information is not found!");return false;}else if (row_num != 1) {DLOG("the user information queried is not unique!");return false;}MYSQL_ROW row = mysql_fetch_row(res);// 5. 将数据库中的用户信息填写到user对象中去,作为输出型参数// 这里有细节,因为结果集中的数据都是字符串,所以转化为整型,最好是长整型// 但是转化为长整型会报错,所以再强转为json的数据类型,如下面的Json::UInt64user["id"] = (Json::UInt64)std::stol(row[0]);user["score"] = (Json::UInt64)std::stol(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);// 6. 别忘了释放结果集mysql_free_result(res);return true;}// 通过用户名获取用户信息bool select_by_name(const std::string& name, Json::Value& user){
#define SELECT_BY_NAME "select id, score, total_count, win_count from user where username='%s';"// 1. 将语句格式化后放到query数组中char query[4096] = {0};sprintf(query, SELECT_BY_NAME, name.c_str());// 2. 执行语句,并且保存结果集到本地 -- 因为是查询语句,所以还是依然要加锁保证线程安全MYSQL_RES* res = nullptr;{std::unique_lock<std::mutex> lock(_mtx); // 相当于加锁// 执行语句bool ret = mysql_util::mysql_exec(_mysql, query);if(ret == false){DLOG("get user by name failed!!");return false;}// 保存结果集res = mysql_store_result(_mysql);if(res == nullptr){DLOG("mysql_store_result failed");return false;}}// 3. 获取结果集条数,得到结果集int row_num = mysql_num_rows(res);if(row_num == 0){DLOG("the user information is not found!");return false;}else if(row_num != 1){DLOG("the user information queried is not unique!!");return false;}MYSQL_ROW row = mysql_fetch_row(res);// 4. 将数据库中的用户信息填写到user对象中去,作为输出型参数user["id"] = (Json::UInt64)std::stol(row[0]);user["username"] = name;user["score"] = (Json::UInt64)std::stol(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);// 5. 释放结果集mysql_free_result(res);return true;}// 通过id获取用户信息bool select_by_id(uint64_t id, Json::Value& user){
#define SELECT_BY_ID "select username, score, total_count, win_count from user where id='%d';"// 1. 将语句格式化后放到query数组中char query[4096] = {0};sprintf(query, SELECT_BY_ID, id);// 2. 执行语句,并且保存结果集到本地 -- 因为是查询语句,所以还是依然要加锁保证线程安全MYSQL_RES* res = nullptr;{std::unique_lock<std::mutex> lock(_mtx); // 相当于加锁// 执行语句bool ret = mysql_util::mysql_exec(_mysql, query);if(ret == false){DLOG("get user by id failed!!");return false;}// 保存结果集res = mysql_store_result(_mysql);if(res == nullptr){DLOG("mysql_store_result failed");return false;}}// 3. 获取结果集条数,得到结果集int row_num = mysql_num_rows(res);if(row_num == 0){DLOG("the user information is not found!");return false;}else if(row_num != 1){DLOG("the user information queried is not unique!!");return false;}MYSQL_ROW row = mysql_fetch_row(res);// 4. 将数据库中的用户信息填写到user对象中去,作为输出型参数user["id"] = (Json::UInt64)id;user["username"] = row[0];user["score"] = (Json::UInt64)std::stol(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);// 5. 释放结果集mysql_free_result(res);return true;}// 胜利处理函数 -- 胜利时天梯分数增加30分,战斗场次增加1,胜利场次增加1bool win(uint64_t id){
#define WIN "update user set score=score+30, total_count=total_count+1, win_count=win_count+1 where id='%d';"// 1. 将语句格式化后放到query数组中char query[4096] = {0};sprintf(query, WIN, id);// 2. 执行语句bool ret = mysql_util::mysql_exec(_mysql, query);if(ret == false){DLOG("update win user info failed!!\n");return false;}return true;}// 失败处理函数 -- 失败时天梯分数减少30,战斗场次增加1,其他不变bool lose(uint64_t id){
#define LOSE "update user set score=score-30, total_count=total_count+1 where id='%d';"// 1. 将语句格式化后放到query数组中char query[4096] = {0};sprintf(query, LOSE, id);// 2. 执行语句bool ret = mysql_util::mysql_exec(_mysql, query);if(ret == false){DLOG("update lose user info failed!!\n");return false;}return true;}
};#endif