C++通用数据库操作类实现

业务中涉及到要实现数据库操作的通用化,想了一种思路,欢迎讨论

提到通用性,然后又用c++,这两个放到一起总是让人抓狂。。

业务中有些服务需要进行数据同步,无非就是将一个表中的数据抽取出来,放到另一个表中对应好,而这块业务又经常会用到,原来的方法是每个不同的业务分别开发一个对应的数据同步服务,理论上是没错,但是业务量上去了之后,每次开发一个,显得很是繁琐,于是就想到能不能一次开发,多次使用,不同的业务放到配置文件里去配置。

捋了一下之后,每个不同的服务不同之处只是操作的表不一样,如果能把这一层抽出来就好了,但是c++的数据库操作,不管是ADO,ODBC,还是OCI,解析sql语句的时候只能根据不同的表定义不同的结构体,然后根据字段类型定义变量类型,然后再跟变量绑定。

能不能有一种方法,将表结构和字段类型都定义成通用的呢,通用,表结构该怎么定义呢?

首先表中字段不确定个数,那么自然就想到动态数组vector,但是字段类型跟字段值怎么办呢,怎么将字段名,字段类型,字段值这三个对应起来然后存储,然后就想到了用三个vector分别对应这三个,但是怎么来保证三个vector映射起来呢,一般的map结构只能映射两个,一对一的关系,不爽,那我就自定义一个map类,将三个vector映射起来,通过定义一些用的着的成员函数分别操作三个成员字段名,字段类型,字段值的vector,这样就完成了映射。一个map结构就对应了表里的一行记录。自定义的map类如下:

#pragma once
#pragma warning(disable:4996)
#include <DEQUE>
#include <ASSERT.H>

//=============================================================================
/**
*  Map table.
*  Edit by Mr.zhu 2016/12/5
*/
template<class T1, class T2, class T3>
class CTableMap
{
public:
	// T1 ==> T2
	T2 First_to_Second(const T1& t1, const T2& t2Default) const
	{
		int   i = GetIndexT1(t1);
		return (i == -1) ? t2Default : m_t2Tab[i];
	}

	// T2 ==> T1
	T1 Second_to_First(const T2& t2, const T1& t1Default) const
	{
		int   i = GetIndexT2(t2);
		return (i == -1) ? t1Default : m_t1Tab[i];
	}

	// T1 ==> T3
	T3 First_to_Third(const T1& t1, const T3& t3Default) const
	{
		int  i = GetIndexT1(t1);
		return (i == -1) ? t3Default : m_t3Tab[i];
	}

	// 获取元素个数.
	int SizeCol() const
	{
		if (m_t1Tab.size() == m_t2Tab.size())
			return (int)m_t1Tab.size();
		assert(false);
		return 0;
	}
	int SizeValue() const
	{
		if (m_t1Tab.size() == m_t2Tab.size() && m_t1Tab.size() == m_t3Tab.size())
			return (int)m_t1Tab.size();
		assert(false);
		return 0;
	}

	// 查找元素T1索引号,找不到返回-1
	int GetIndexT1(const T1& t1) const
	{
		for (size_t i = 0; i < m_t1Tab.size(); i++)
		{
			if (m_t1Tab[i] == t1)
				return (int)i;
		}
		return -1;
	}
	// 查找元素T2索引号,找不到返回-1.
	int GetIndexT2(const T2& t2) const
	{
		for (size_t i = 0; i < m_t2Tab.size(); i++)
		{
			if (m_t2Tab[i] == t2)
				return (int)i;
		}
		return -1;
	}
	// 查找元素T3索引号,找不到返回-1.
	int GetIndexT3(const T3& t3) const
	{
		for (size_t i = 0; i < m_t3Tab.size(); i++)
		{
			if (m_t3Tab[i] == t3)
				return (int)i;
		}
		return -1;
	}

	// 按索引号获取T1.
	const T1& GetT1(int n) const { assert(IsValidIndex(n)); return m_t1Tab[n]; }
	T1& GetT1(int n) { assert(IsValidIndex(n)); return m_t1Tab[n]; }
	// 按索引号获取T2.
	const T2& GetT2(int n) const { assert(IsValidIndex(n)); return m_t2Tab[n]; }
	T2& GetT2(int n) { assert(IsValidIndex(n)); return m_t2Tab[n]; }
	// 按索引号获取T3
	const T3& GetT3(int n) const { assert(IsValidIndex(n)); return m_t3Tab[n]; }
	T3& GetT3(int n) { assert(IsValidIndex(n)); return m_t3Tab[n]; }

	// 增加元素.
	void AddElement(const T1& t1, const T2& t2, const T3& t3)
	{
		m_t1Tab.push_back(t1);
		m_t2Tab.push_back(t2);
		m_t3Tab.push_back(t3);
	}

	// 按索引删除元素.
	void RemoveElement(int nIndex)
	{
		if (IsValidIndex(nIndex))
		{
			m_t1Tab.erase(m_t1Tab.begin() + nIndex);
			m_t2Tab.erase(m_t2Tab.begin() + nIndex);
			m_t3Tab.erase(m_t3Tab.begin() + nIndex);
		}
		else
		{
			assert(false);
		}
	}
	// 删除指定元素.
	void RemoveElement(const T1& t1, const T2& t2, const T3& t3)
	{
		for (int i = 0; i < GetElementCount(); i++)
			if ((m_t1Tab[i] == t1) &&
				(m_t2Tab[i] == t2) &&
				(m_t3Tab[i] == t3) )
			{
				RemoveElement(i);
				return; // 只能return不能break.
			}
	}

	// 清空map.
	void Clear()
	{
		m_t1Tab.clear();
		m_t2Tab.clear();
		m_t3Tab.clear();
	}

	// 弹出第一个元素.
	void PopFront()
	{
		if (GetElementCount())
		{
			m_t1Tab.pop_front();
			m_t2Tab.pop_front();
			m_t3Tab.pop_front();
		}
	}

	// 增加T1,T2
	void AddElement(const T1& t1, const T2& t2)
	{
		m_t1Tab.push_back(t1);
		m_t2Tab.push_back(t2);
	}

	// 增加T3
	void AddElement(const T3& t3)
	{
		m_t3Tab.push_back(t3);
	}
	// 清空value列
	void ClearT3()
	{
		m_t3Tab.clear();
	}

private:
	bool IsValidIndex(int nIndex) const
	{
		return (nIndex >= 0) && (nIndex < GetElementCount());
	}

private:
	// c++标准建议:
	// vector是那种应该在默认情况下使用的序列。
	// 如果大多数插入和删除操作发生在序列的头部或尾部时,应该选用deque。
	std::deque<T1>   m_t1Tab;
	std::deque<T2>   m_t2Tab;
	std::deque<T3>	 m_t3Tab;
};

结构是定义好了,字段名跟字段值可以用string来定义,但是字段值怎么办呢,如果存一个值必须要有一个类型,才能存储吧,但是字段值的类型我不确定啊,要是有一个通用的类型,可以接收任意类型的数据就好了,c++里没有这种类型啊,咋办?有人说void*,void*是可以,但是类型不安全,转来转去就转崩溃了,没有就自己搞,万幸的是除了STL,还有前人整的boost库,里面有一个any类型,恰好就实现了通用类型,而且是类型安全的,好的,那我们就用一用吧,这样我门在定义表里的一行数据时就可以定义了:

CTableMap<string, string, boost::any>    a_row;  // 声明一个tablemap结构

表里一行数据定义好了,整个表里的怎么办呢,放到list里:

std::list< CTableMap<string, string, boost::any> >  a_table;  // 声明一个表结构

OK,搞定,这样我们就找到了一个合适的跟数据类型无关的结构来存放表数据了,既然跟类型无关了,那跟数据库里数据操作相关的select和insert语句,我们也可以写成通用了,把sql语句抽出来放到配置文件里,我们只需要把sql语句从配置读出来,然后select到的数据存放到listmap里,insert的时候再从listmap里取,这样就实现了通用性。


接下来就是一些优化了,以及如何让服务更健壮,更松耦合,balabala。。一堆程序设计类设计上的问题

首先,STL中list这个结构我看着不爽,操作起来太繁琐,而且如果是多线程,也没有锁之类的来保证安全,不爽就重写!我想实现两个目的,一个是我数据放到list里我就不想管了,从list里拿数据之后,我就把这条数据从list里剔除,另一个就是要保证线程的安全性,把这两个功能加上,那我用起来就带劲多了,只需要往里放和往外拿就可以了,其他的管他呢。好吧,那就实现下吧:

#pragma once
#pragma warning(disable:4996)

#include <ace/Thread_Mutex.h>
#include <ace/Recursive_Thread_Mutex.h>
#include <ace/Method_Request.h>
#include <ace/Condition_T.h>
#include <ace/Atomic_Op.h>
#include <list>
using std::list;


template<class Type>
class CSimpleThreadQueue
{
public:
	typedef typename std::list<Type>::iterator QueuePosition;

public:
	CSimpleThreadQueue() : m_cond(m_mutex) {};
	  ~CSimpleThreadQueue(){};
	  
	// Enqueue Item
	int enqueue(Type &item);

	// Dequeue Item.
	int dequeue(Type &item, ACE_Time_Value *tv = 0);

	// Enqueue Head
	int enqueue_head(Type item);

	// Return Queue Size
	size_t size();
private:
	list<Type> m_list;		// List To Store Obj.
	ACE_Thread_Mutex m_mutex; // Thread Mutex
	ACE_Thread_Condition<ACE_Thread_Mutex> m_cond; // Thread Condition
};

/**
*  Dequeue Item.
*
*  @return pointer of ACE_Method_Request object.
*/
template<class Type>
int CSimpleThreadQueue<Type>::dequeue(Type &item, ACE_Time_Value *tv)
{
	int nSize = 0;
	while (true)
	{
		m_mutex.acquire();
		if (! m_list.empty() )
			break;
		
		int nErr = m_cond.wait(tv);
		m_mutex.release();
		if (nErr == -1 && ACE_OS::last_error() == ETIME)
		{
			return -1;
		}
	}
	
	item = m_list.front();
	m_list.pop_front();
	nSize = m_list.size();
	//-- queued_action_count_;
	m_mutex.release();

	return 0;
}

/**
*  Enqueue Head.
*
*  @param  -[in]  Type*  item: [ item to enqueue]
*/
template<class Type>
int CSimpleThreadQueue<Type>::enqueue_head(Type item)
{
	int nSize = 0;
	m_mutex.acquire();
	m_list.push_front(item);
	nSize = m_list.size();
	m_cond.signal();
	//++ total_action_count_;
	//++ queued_action_count_;
	m_mutex.release();
	return nSize;
}


/**
*  Enqueue Item.
*
*  @param  -[in]  Type*  item: [ item to enqueue]
*/
template<class Type>
int CSimpleThreadQueue<Type>::enqueue(Type &item)
{
	int nSize = 0;
	m_mutex.acquire();
	m_list.push_back(item);
	nSize = m_list.size();
	m_cond.signal();
	//++ total_action_count_;
	//++ queued_action_count_;
	m_mutex.release();
	return nSize;
}

/**
 *  Return Queue Size.
 *
 */
template<class Type>
size_t CSimpleThreadQueue<Type>::size()
{
//	size_t nSize;
//	mutex_.acquire();
//	nSize = list_.size();
//	mutex_.release();
//	return size();
	return m_list.size();
}

OK,结构优化凭自己的半吊子水平就只能做到这一步了,接下来业务优化,首先捋一捋,总共可以分成这么基层,读配置层,定时读数据层,然后存数据层,为了显得高大上一些,那再加个线程管理层吧,至于线程池啥的,也先画上(虽然完全用不着),这样整个框架也捋清楚了,画一下流程图吧那就。

数据抽取服务结构图如下




OK了,整个架子东拼西凑的也算搭起来了,接下来就是代码实现了,核心的无非就是从一个表里把数据读出来,然后放到listmap里,然后再从listmap里把数据都出来“插”入另一个表里,写吧那就(我这里用的是oralib这个对oci二次封装之后的操作类,至于OTL或者其它高大上的也完全可以):


bool CDBOper::GetData()
{
	char chSQL[1024] = { 0 };
	statement* pStm = NULL;
	resultset* pRets = NULL;

	try {
		//sprintf(chSQL, "select n, d, s from oralib_test where s > %s", strRKSJ.c_str());
		sprintf(chSQL, GLOBALCFGOBJ->m_strSelectSQL.c_str(), GLOBALCFGOBJ->m_strStarttime.c_str());

		CVLog.LogMessage(LOG_LEVEL_INFO, "chSQL:%s", chSQL);
		pStm = m_dbConn.prepare(chSQL);
		if (pStm == NULL)
		{
			CVLog.LogMessage(LOG_LEVEL_ERROR, "GetData 无法取得数据库连接指针!");
			return false;
		}

		pRets = pStm->select();

		while (!pRets->eod())
		{
			CTableMap<string, string, virtual_type> data_map;
			for (int i = 0; i < DBTHREADOBJ->m_dbColumMap.SizeCol(); i++)
			{
				string col_name = DBTHREADOBJ->m_dbColumMap.GetT1(i);
				string col_type = DBTHREADOBJ->m_dbColumMap.GetT2(i);
				virtual_type col_value;

				if (col_type == "varchar2")
				{
					string a_value = (string)(Pstr)(*pRets)[col_name.c_str()];
					col_value = a_value;
				}
				if (col_type == "number")
				{
					int a_value = (int)(*pRets)[col_name.c_str()];
					col_value = a_value;
				}
				if (col_type == "date")
				{
					string a_value = (string)(Pstr)(*pRets)[col_name.c_str()];
					col_value = a_value;
				}
				if (col_type == "float6")
				{
					string a_value = (string)(Pstr)(*pRets)[col_name.c_str()];
					col_value = a_value;
				}

				data_map.AddElement(col_name, col_type, col_value);
			}
			DBTHREADOBJ->m_rowlist.enqueue(data_map);

			pRets->next();
		}
	}
	catch (oralib::error &e)
	{
		string szErr = e.details();
		CVLog.LogMessage(LOG_LEVEL_ERROR, "<GetData> 获取违法信息错误:%s; SQL=%s!", szErr, chSQL);
		return false;
	}
	catch (...)
	{
		CVLog.LogMessage(LOG_LEVEL_ERROR, "<GetData> Unknown DB error; SQL=%s!", chSQL);
	}

	if (pRets != NULL)
		pRets->release();
	if (pStm != NULL)
		pStm->release();

	return true;
}

bool CDBOper::SaveData(int nthreads, CTableMap<string,string,virtual_type> tdataMap)
{
	//判断数据库连接状态
	if (false == TestDBConnection())
	{
		CVLog.LogMessage(LOG_LEVEL_ERROR, "线程%d数据库连接失败!,保存数据丢失", nthreads);
		return false;
	}


	char* szSQL = new char[1024 * 1024];
	//string strInsertSQL = GLOBALCFGOBJ->m_strInsertSQL;
	string strTmpSql;
	string strSQL;
	size_t nStartPos = 0; // 字符串截取起始位置

	for (int i = 0; i < tdataMap.SizeValue(); i++)
	{
		string col_name = tdataMap.GetT1(i);
		string col_type = tdataMap.GetT2(i);
		virtual_type col_value;
		col_value = tdataMap.GetT3(i);
		if (col_type == "varchar2")
		{
			string str_value = col_value.cast<string>();
			strTmpSql = ConvertN(m_strInsertSQL, nStartPos, "'%s'");
			sprintf(szSQL, strTmpSql.c_str(), str_value.c_str());
		}
		if (col_type == "number")
		{
			int n_value = col_value.cast<int>();
			strTmpSql = ConvertN(m_strInsertSQL, nStartPos, "%d");
			sprintf(szSQL, strTmpSql.c_str(), n_value);
		}
		if (col_type == "date")
		{
			string str_value = col_value.cast<string>();
			strTmpSql = ConvertN(m_strInsertSQL, nStartPos, "'%s'");
			sprintf(szSQL, strTmpSql.c_str(), str_value.c_str());
		}
		if (col_type == "float6")
		{
			string str_value = col_value.cast<string>();
			string sfloat_value = ConvertN(str_value, 6);
			strTmpSql = ConvertN(m_strInsertSQL, nStartPos, "'%s'");
			sprintf(szSQL, strTmpSql.c_str(), str_value.c_str());
		}

		strSQL += szSQL;
	}
	//strTmpSql = ConvertN(strInsertSQL, nStartPos, "");
	strTmpSql = m_strInsertSQL.substr(nStartPos, m_strInsertSQL.length() - nStartPos);
	strSQL += strTmpSql;

	CVLog.LogMessage(LOG_LEVEL_CRITICAL, "chSQL (%s)!", strSQL.c_str());

	sprintf(szSQL, "%s", strSQL.c_str());

	if (!ExeSQL(szSQL, true))
	{
		CVLog.LogMessage(LOG_LEVEL_CRITICAL, "线程%d写入目的表失败 (%s)!", nthreads, szSQL);
		return false;
	}
	else
	{
		CVLog.LogMessage(LOG_LEVEL_INFO, "线程%d写入目的表成功 (%s)!", nthreads, szSQL);
	}

	if (szSQL != NULL)
	{
		delete szSQL;
		szSQL = NULL;
	}

	return true;
}

这里面还有一些不足,就是从sql读出来的sql语句做解析的时候,我是按照占位符解析的,找到一个占位符就把这个占位符跟前面的字符串截出来,然后用spintf将变量拼进去,并记录下截取的位置,下一个字段过来的时候再截,至于为啥这么做,因为spintf没办法前面多个占位符,后面只传给它一个变量,那为啥不用replace非要用sprintf呢,因为replace的执行效率是不能忍的。

有没有其他更好的方法?暂时凭自己的智商只能想到这个笨办法了。

到这里总算搞定了,算了算用了两天的时间,可以接收(虽然根据一个业务专门开发一个数据同步服务也需要两天时间,呵呵),暂时就做成这样吧,至于以后如果设计模式方面有什么新的理解再来重构。

如果有需要整个工程的童鞋,请留下你的邮箱,我看到后会发过去,包括带源码的(ACE库,boost库,oralib库)。

展开阅读全文

没有更多推荐了,返回首页