POCO数据库操作简介
按Poco的文档,本文缩写、改编、注释POCO Data User Guide的内容,介绍Poco的数据库操作。原文见http://pocoproject.org/docs/00200-DataUserManual.html。
先介绍一个简单例子,无须解释,其意自明。
#include "Poco/Data/SQLite/Connector.h"
#include <iostream>
usingnamespace Poco::Data;
void init()
{
SQLite::Connector::registerConnector();
}
void shutdown()
{
SQLite::Connector::unregisterConnector();
}
int main(int argc, char* argv[])
{
init();
Session ses("SQLite", "sample.db");
int count =0;
ses <<"SELECT COUNT(*) FROM PERSON", into(count), now;
std::cout <<"People in DB "<< count;
shutdown();
}
表示与数据库的连接,每个Session有两个要素,一是Poco表示数据库种类的类型标识,二是连接每种数据库所需的一个字符串。注意,实际的软件中这两个参数一般不会硬编码在代码中。
以SQLite为例
或
Session ses(SessionFactory::instance()::create("SQLite","sample.db"));
读取和写入数据:占位符placeholder,use(),into()
写入操作中使用placeholder和use()
ses <<"INSERT INTO FORENAME VALUES(:name)", use(aName), now;
读取操作中使用placeholder和into()
ses <<"SELECT NAME FROM FORENAME", into(aName), now; // the default is the empty string
ses <<"SELECT NAME FROM FORENAME", into(aName, "default"), now;
也可以同时使用use()和into()
std::string match("Peter")
ses <<"SELECT NAME FROM FORENAME WHERE NAME=:name", into(aName), use(match), now;
poco_assert (aName == match);
一般情况下,数据表有多列,用如下形式
std::string lastName("Junior");
int age =0;
ses << INSERT INTO PERSON VALUES (:fn, :ln, :age)", use(firstName), use(lastName), use(age), now;
ses <<"SELECT (firstname, lastname, age) FROM Person", into(firstName), into(lastName), into(age), now;
注意,placeholder与use或into的顺序是一一对应的。
Statement用于表示SQL语句
原型
Statement Session::operator<< (const T& t)
两种赋值方式,第一种方式会立即执行语句
Statement stmt = ( ses <<"INSERT INTO FORENAME VALUES(:name)", use(aName) );
第二种赋值方式可以把语句保存起来,稍后再执行
Statement stmt = ( ses <<"INSERT INTO FORENAME VALUES(:name)", use(aName) );
stmt.execute();
poco_assert (stmt.done());
注意,包含占位符的语句只是一个字符串,Statement可以缓存起来,并可以稍后执行,再使用时可以复用一些语句,可以选择Statement的执行时机。于是有了所谓的predefined statement
Predefined Statement
所谓predefined statement即是句尾没有使用now的语句,如
用法如下
Statement stmt = ( ses <<"INSERT INTO FORENAME VALUES(:name)", use(aName) );
for (int i =0; i <100; ++i)
{
aName.append("x");
stmt.execute();
}
通过这样的操作,可以把一个复杂的算法用SQL语句实现,并包装成一个C++的函数,用数据库的操作完成大量的计算,这是一个非常有用的功能,比如上面的一段代码可以转化成函数形式
{
std::string aName();
Statement stmt = ( ses <<"INSERT INTO FORENAME VALUES(:name)", use(aName) );
for (vector<string>::const_iteartor it = str.begin(), e = str.end(); it != e; ++it)
{
aName.append(*it);
stmt.execute();
}
}
不愧为用C++封的数据操作,与C++代码几乎无缝连接,下面讲到使用C++的容器类型时,这种特征更加明显。
说明:
use()的输入参数是一个引用,而不可以是一个常量,下面的代码是错的
stmt.execute();
对容器类型的支持
如果与操作DB是使用容器类型,Poco要求对放入容器中的类型具有如下的要求
vector: no requirements
set: the < operator must be supported by the datatype. Note that duplicate key/value pairs are ignored.
multiset: the < operator must be supported by the datatype
map: the () operator must be supported by the datatype and return the key of the object. Note that duplicate key/value pairs are ignored.
multimap: the () operator must be supported by the datatype and return the key of the object
用容器类接收或是导出数据的例子如下:
std::vector<std::string> data;
for (int i =0; i <100; ++i)
{
aName.append("x");
data.push_back(aName);
}
ses <<"INSERT INTO FORENAME VALUES(:name)", use(data), now;
导出数据的例子,注意,如果数据表中有多个表项,确用一个字符串接受,会抛出异常。
std::vector<std::string> names;
ses << "SELECT NAME FROM FORENAME", into(names), now
The limit clause
数据库中的表项很多,具体取回多少表项用limit clause提定,共有四个limit, lowerLimit, upperLimit和rang
limit
ses <<"SELECT NAME FROM FORENAME", into(names), limit(50), now
lowerLimit
ses <<"SELECT NAME FROM FORENAME", into(aName), lowerLimit(1), now;
upperLimit
Statement stmt = (ses <<"SELECT NAME FROM FORENAME", into(aName), lowerLimit(1), upperLimit(1));
while (!stmt.done())
stmt.execute();
range
Statement stmt = (ses <<"SELECT NAME FROM FORENAME", into(aName), range(1,1));
while (!stmt.done())
stmt.execute();
将复杂数据结构映射成数据表
将对象转化成数据表,有两 种思路,一是面向对象的数据库,即在数据库支持部分面向对象的操作,二是用编译语言实现对对象到数据表的映射,称为object relation map,即ORM,实现时关键时把变量名映射为数据表的字段名,所以ORM在脚本语言中比较容易实现,因为有些脚本语言有自反机制。
Poco实现了一种映射,虽不是特别优雅,但对C++来讲已经足够好了。思路是为每一个类实现一个存取类对象的模板,很类似于序列化操作。举例如下,如果类定义是
{
public:
// default constructor+destr.
// getter and setter methods for all members
[...]
booloperator<(const Person& p) const
/// we need this for set and multiset support
{
return _socialSecNr < p._socialSecNr;
}
Poco::UInt64 operator()() const
/// we need this operator to return the key for the map and multimap
{
return _socialSecNr;
}
private:
std::string _firstName;
std::string _lastName;
Poco::UInt64 _socialSecNr;
}
为了方便读写这个类对象,要实现一个类模板
namespace Data { // 类模板要放在Poco::Data中
template <>
class TypeHandler<class Person>
{
public:
static std::size_t size()
{
return3; // we handle three columns of the Table!
}
staticvoid bind(std::size_t pos, const Person& obj, AbstractBinder* pBinder)
{
poco_assert_dbg (pBinder !=0);
// the table is defined as Person (FirstName VARCHAR(30), lastName VARCHAR, SocialSecNr INTEGER(3))
// Note that we advance pos by the number of columns the datatype uses! For string/int this is one.
TypeHandler<std::string>::bind(pos++, obj.getFirstName(), pBinder);
TypeHandler<std::string>::bind(pos++, obj.getLastName(), pBinder);
TypeHandler<Poco::UInt64>::bind(pos++, obj.getSocialSecNr(), pBinder);
}
staticvoid prepare(std::size_t pos, const Person& obj, AbstractPreparation* pPrepare)
{
poco_assert_dbg (pBinder !=0);
// the table is defined as Person (FirstName VARCHAR(30), lastName VARCHAR, SocialSecNr INTEGER(3))
// Note that we advance pos by the number of columns the datatype uses! For string/int this is one.
TypeHandler<std::string>::prepare(pos++, obj.getFirstName(), pPrepare);
TypeHandler<std::string>::prepare(pos++, obj.getLastName(), pPrepare);
TypeHandler<Poco::UInt64>::prepare(pos++, obj.getSocialSecNr(), pPrepare);
}
staticvoid extract(std::size_t pos, Person& obj, const Person& defVal, AbstractExtractor* pExt)
/// obj will contain the result, defVal contains values we should use when one column is NULL
{
poco_assert_dbg (pExt !=0);
std::string firstName;
std::string lastName;
Poco::UInt64 socialSecNr =0;
TypeHandler<std::string>::extract(pos++, firstName, defVal.getFirstName(), pExt);
TypeHandler<std::string>::extract(pos++, lastName, defVal.getLastName(), pExt);
TypeHandler<Poco::UInt64>::extract(pos++, socialSecNr, defVal.getSocialSecNr(), pExt);
obj.setFirstName(firstName);
obj.setLastName(lastName);
obj.setSocialSecNr(socialSecNr);
}
};
} } // namespace Poco::Data
这个设计方案的好处在于,实现序列化的类不必干涉原有类的设计,缺点是依赖于字段在数据表中的相对位置,而数据库是建立在关系代数基础上的,并不重视这种位置(顺便提一句Wt中实现的映射则比这种实现方法好得多)。这种设计并不十分巧妙。
使用时如下:
ses <<"SELECT * FROM Person", into(people), now;
RecordSet
从介绍中可以看出,这个是 Poco::Data设计的一个核心类型,从中也可以猜出序列化具体类型时有可能就是用本类实现的。该类可以操纵数据库操作结果的结果,抽象为一个二维表 (这是和数据库的概念有出入的,数据库的概念是集合,而不是二维表格,也就是说列之间是无序的,但RecordSet类依赖这种顺序)。
select <<"SELECT * FROM Person";
select.execute();
RecordSet rs(select);
bool more = rs.moveFirst();
while (more)
{
for (std::size_t col =0; col < cols; ++col)
{
std::cout << rs[col].convert<std::string>() <<"";
}
std::cout << std::endl;
more = rs.moveNext();
}
如果结合limit使用则如下
select <<"SELECT * FROM Person", range(0, 10);
RecordSet rs(select);
while (!select.done())
{
select.execute();
bool more = rs.moveFirst();
while (more)
{
for (std::size_t col =0; col < cols; ++col)
{
std::cout << rs[col].convert<std::string>() <<"";
}
std::cout << std::endl;
more = rs.moveNext();
}
}
Tuples
前面介绍了一个类对象如何序列化为数据库中的一组数据,是比较麻烦的,但利用Tuples就不那么麻烦了:
typedef std::vector<Person> People;
People people;
people.push_back(Person("Bart Simpson", "Springfield", 12));
people.push_back(Person("Lisa Simpson", "Springfield", 10));
Statement insert(session);
insert <<"INSERT INTO Person VALUES(:name, :address, :age)", use(people), now;
Statement select(session);
select <<"SELECT Name, Address, Age FROM Person", into(people), now;
for (People::const_iterator it = people.begin(); it != people.end(); ++it)
{
std::cout <<"Name: "<< it->get<0>() <<
", Address: "<< it->get<1>() <<
", Age: "<< it->get<2>() <<std::endl;
}
从这个思路出发,可以设计一个存取对象的方法,这样C++的类定义与在数据库中的建表的工作就可以放在一起了,并可以保持一致
{
public:
typedef Poco::Tuple<std::string, std::string, int> PersonData;
Person(const PersonData & data)
{
_data = data;
}
Person(Session & session)
{
Statement select(session);
select <<"SELECT Name, Address, Age FROM Person", into(_data), now;
//用 PersonField改成更灵活的形式
}
void save(Session & session) const
{
Statement insert(session);
insert <<"INSERT INTO Person VALUES(:name, :address, :age)", use(_data), now;
}
private:
PersonData _data
};
const map<string, int> PersonField = { {“Name”, 0}, {“Address”, 1}, {“Age”, 2} };
// 最好在此外可以触发一个建立数据表的操作
Session Pooling
用于保存与数据库的连接,应用程序只起动少数几个连接,能重用时尽量重用,保存在SessionPool中,这样就把对一个数据库的访问(session)做为全局变量在整个应用程序中使用。
// ...
Session sess(pool.get());