SQL的执行可大致分为下面两种模式:
“Immediate Statements” VS “Prepared Staements” :
动态的根据传入的参数拼接SQL语句并执行,一条语句经过MySQL server层分析器、优化器、执行器组件,分别进行词法、语义解析、优化SQL语句、选择索引、制定执行计划、执行并返回结果。
对SQL语句进行词法语义分析、优化SQL语句、选择索引、制定执行计划等一系列操作,称为 “对SQL语句的编译”。
如上,一条SQL语句按照此流程处理,一次编译,单次运行,此类普通语句被称作 “Immediate Statements”(即时SQL)。
例如:
bool CUserModel::getUser(uint32_t nUserId, DBUserInfo_t &cUser)
{
CDBConn* pDBConn = CDBManager::getInstance()->GetDBConn("teamtalk_slave");
if(pDBConn)
{
//根据函数外部传入的参数 nUserId,动态构造 select查询语句并执行:
string strSql = "select * from IMUser where id = " + int2string(nUserId);
CResultSet* pResultSet = pDBConn->ExcuteQuery(strSql.c_str());
if(pResultSet)
{
while(pResultSet->Next())
{
//...
}
}
}
}
但是,绝大多数情况下,一般会需要一条SQL语句反复调用执行(例如上面的查找IMUser表中的用户信息,每次客户端向服务器请求登录验证时都需要执行一次),或者每次执行的时候只有个别的值不同(比如select的where子句值不同,update的set子句值不同,insert的values子句值不同)。
如果每次都需要经过上面的SQL编译过程(词法语义分析、语句优化、制定执行计划等),则效率明细会受到影响。
所谓 “预编译SQL语句”,就是将此类SQL语句中的某些值使用 “占位符” 替代,可以视为将SQL语句 “模板化” 或者说 “参数化”。一般称这类语句为 “Prepared Statements”。
预编译SQL语句的优势在于:一次编译、多次运行,省去了解析、优化等过程。此外使用预编译SQL语句还能防止SQL注入,下文展开。
(1)先与MySQL数据库取得连接,获得 “连接句柄” MYSQL*
:
MYSQl* mysql_init();
mysql_options();
mysql_real_connect(MYSQL*, ip, user_name, passed, db_name, port);
(2)基于这个 MYSQL*
连接句柄,初始化一个“预编译句柄”MYSQL_STMT*
:
MYSQL_STMT* mysql_stmt_init(MYSQL*);
(3)传入准备好的带有“占位符”的SQL语句,进行编译:
mysql_stmt_prepare(MYSQL_STMT*, sql.c_str(), sizeof(sql));
(4)在后面要使用这个预编译的SQL语句时,需要向其中传入实参填补“占位符”,所以我们必须要先将占位符的个数统计出来,并预先初始化一个 MYSQL_BIND
类型的结构体数组
(MYSQL_BIND[]数组的元素个数是SQL语句中占位符的个数,数组中每个元素是MYSQL_BIND结构体,用于指定某个占位符上的数据类型(如int) 及 数据值),等待使用时向其中填充参数:
uint32_t m_param_cnt = mysql_stmt_param_count(MYSQL_STMT*);
MYSQL_BIND* m_param_bind = new MYSQL_BIND[m_param_cnt]; //新建一个数组
(5)在使用时,先给 MYSQL_BIND[]
数组填充值:
for(int index = 0; index < m_param_cnt; index++)
{
//如果value是int型:
MYSQL_BIND[index].buffer_type = MYSQL_TYPE_LONG;
MYSQL_BIND[index].buffer = &value;
/*
//如果value是string型:
MYSQL_BIND[index].buffer_type = MYSQL_TYPE_LONG;
MYSQL_BIND[index].buffer = (char*)value.c_str();
MYSQL_BIND[index].buffer_length = value.size();
*/
}
(6)向填充好实参的MYSQL_BIND数组传入MYSQL_STMT句柄,随后执行这条SQL语句,并检查执行结果:
msyql_stmt_bind_param(m_stmt, m_bind_param);
mysql_stmt_excute(m_stmt); //如果有错误发生,函数返回非0,使用 mysql_stmt_error(m_stmt);可检查错误原因
mysql_stmt_affected_rows(m_stmt) == 0;
实现一个 CPrepareStatement
类,封装 MYSQL_STMT* 和 MYSQL_BIND* 对象,即相应的SQL预编译方法:
//cpreparestatement.h
class CPrepareStatement {
public:
CPrepareStatement() {}
~CPrepareStatement() {}
bool Init(MYSQL* mysql, string& sql);
void SetParam(uint32_t index, int& value);
void SetParam(uint32_t index, uint32_t& value);
void SetParam(uint32_t index, string& value);
void SetParam(uint32_t index, const string& value);
bool ExecuteUpdate();
uint32_t GetInsertId();
private:
MYSQL_STMT* m_stmt;
MYSQL_BNID* m_param_bind;
uint32_t m_param_cnt;
};
//cpreparement.cpp
bool CPrepareStatement::Init(MYSQL* mysql, string& sql) {
mysql_ping(mysql);
m_stmt = mysql_stmt_init(mysql);
if(!m_stmt) {
return false;
}
if(mysql_stmt_prepare(m_stmt, sql.c_str(), sql.size())) {
printf("%s\n", mysql_stmt_error(m_stmt));
return false;
}
m_param_cnt = mysql_stmt_papram_count(m_stmt);
if(m_param_cnt > 0) {
m_param_bind = new MYSQL_BIND[m_param_cnt];
if(!m_param_bind) {
return false;
}
}
memset(m_param_bind, 0, sizeof(MYSQL_BIND) * m_param_cnt);
return true;
}
//注意:给int型和string型赋值的方式是不同的:
void CPrepareStatement::SetParam(uint32_t index, int& value) {
if(index >= m_param_cnt)
return;
m_param_bind[index].buffer_type = MYSQL_TYPE_LONG;
m_param_bind[index].buffer = &value;
}
void CPrepareStatement::SetParam(uint32_t index, uint32_t& value) {
if(index >= m_param_cnt)
return;
m_param_bind[index].buffer_type = MYSQL_TYPE_LONG;
m_param_bind[index].buffer = &value;
}
void CPrepareStatement::SetParam(uint32_t index, string& value) {
if(index >= m_param_cnt)
return;
m_param_bind[index].buffer_type = MYSQL_TYPE_LONG;
m_param_bind[index].buffer = (char*)value.c_str();
m_param_bind[index].buffer_length = value.size();
}
void CPrepareStatement::SetParam(uint32_t index, const string& value) {
if(index >= m_param_cnt)
return;
m_param_bind[index].buffer_type = MYSQL_TYPE_LONG;
m_param_bind[index].buffer = (char*)value.c_str();
m_param_bind[index].buffer_length = value.size();
}
bool CPrepareStatement::ExecuteUpdate() {
if(!m_stmt)
return false;
if(mysql_stmt_bind_param(m_stmt, m_param_bind)) {
printf("%s\n", mysql_stmt_error(m_stmt));
return false;
}
if(mysql_stmt_execute(m_stmt)) {
printf("%s\n", mysql_stmt_error(m_stmt));
return false;
}
if(msyql_affected_rows(m_stmt) == 0) {
printf("no affect\n");
return false;
}
return true;
}
uint32_t CPrepareStatement::GetInsertId() {
return mysql_stmt_insert_id(m_stmt);
}
使用 class CPrepareStatement
类执行insert into插入操作:
bool CMessageModel::sendMessage(uint32_t nRelateId, uint32_t nFromId, uint32_t nToId, IM::BaseDefine::MsgType nMsgType, uint32_t nCreateTime, uint32_t nMsgId, string& strMsgContent) {
CDBConn* pDBConn = CDBManager::getInstance()->GetDBConn("teamtalk_slave");
if(pDBConn) {
string strTableName = "IMMessage_" + int2string(nRelateId % 8);
string strSql = "insert into " + strTableName + " ('relateId', 'fromId', 'toId', 'msgId', 'content', 'status',
'type', 'created', 'updated') values (?, ?, ?, ?, ?, ?, ?, ?, ?)";
shared_ptr<CPrepareStatement> pStmt = make_shared<CPrepareStatement>();
if(pStmt->Init(pDBConn->GetMysql(), strSql)) {
uint32_t nStatus = 0; //表示查询未被删除的记录
pStmt->SetParam(index++, nRelateId);
pStmt->SetParam(index++, nFromId);
pStmt->SetParam(index++, nToId);
pStmt->SetParam(index++, nMsgId);
pStmt->SetParam(index++, strMsgContent);
pStmt->SetParam(index++, nStatus);
pStmt->SetParam(index++, nMsgType);
pStmt->SetParam(index++, nCreateTime);
pStmt->SetParam(index++, nCreateTime);
pStmt->ExecuteUpdate();
}
//delete pStmt; 使用shared_ptr智能指针,不必delete删除
pDBManager->RelDBConn(pDBConn); //这里同样可以使用RAII的方法实现自动释放,在 CDBConn类对象析构的时候释放连接
}
}
MYSQL_BIND()
函数中的参数类型如下表所示,可见 MYSQL_TYPE_LONG
表示的是 4字节的int
型。
# SQL注入与MySQL预编译:
IM项目中只有在 “insert into” 向表中插入数据时,才会使用 CPrepareStatement 预处理,
因为只有这个时候才会发生 “SQL注入”。
而MySQL预处理不仅 可以防止SQL注入,还有提高执行效率的作用:
《“即时SQL” 与 “预处理SQL” 的区别》:
https://www.cnblogs.com/geaozhang/p/9891338.html
参考链接:
https://dev.mysql.com/doc/c-api/5.6/en/c-api-prepared-statement-type-codes.html