C++数据库操作之SOCI

SOCI是一个数据库操作的库,并不是ORM库,它仍旧需要用户编写sql语句来操作数据库,只是使用起来会更加方便,主要有以下几个特点

  1. 以stream方式输入sql语句
  2. 通过into和use语法传递和解析参数
  3. 支持连接池,线程安全

由此可见它只是一个轻量级的封装,因此也有更大的灵活性,后端支持oracle,mysql等,后续示例均基于mysql

安装

git项目地址https://github.com/SOCI/soci

推荐使用cmake编译

git clone git@github.com:SOCI/soci.git
cd soci
mkdir build 
cd build
cmake .. -G "Unix Makefiles" -DCMAKE_INSTALL_PREFIX=/opt/third_party/soci
make
sudo make install

基本查询

假设有如下表单

CREATE TABLE `Person` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `first_name` varchar(64) NOT NULL DEFAULT '',
  `second_name` varchar(64) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

初始化session

using namespace soci;
session sql("mysql", "dbname=test user=your_name password=123456");

第一个参数为使用的后端数据库类型,第二个参数为数据库连接参数,可以指定的参数包括host port dbname user passowrd等,以空格分隔

insert

string first_name = "Steve";
string last_name = "Jobs";
sql << "insert into Person(first_name, last_name)"
    " values(:first_name, :last_name)", 
    use(first_name), use(last_name);
long id;
sql.get_last_insert_id("Person", id)

通过流的方式传递sql语句,用use语法传递参数

其中Person(first_name, last_name)为数据库table名和column名,values(:first_name, :last_name)里的为参数的占位符,这里可以随便书写,get_last_insert_id函数可以获取自增长字段的返回值

需要注意的是use函数里的参数的生命周期,切记不能将函数返回值作为use函数的参数

select

int id = 1;
string first_name;
string last_name;
sql << "select first_name, last_name from Person where id=:id ", 
    use(id), into(first_name), into(last_name);
if (!sql.got_data())
{
    cout << "no record" << endl;
}

这里根据id字段查询first_name和last_name两个字段,并通过into函数将数据复制给变量,got_data()方法可判断是否有数据返回

当id为整数时,sql语句也可以写作sql << "select balabala from Person where id=" << id,但当id为字符串时这样写会报错,因此建议都采用use函数

如果查询结果是多行数据,则需要使用rowset类型并自己提取

rowset<row> rs = (sql.prepare << "select * from Person");
for (rowset<row>::iterator it = rs.begin(); it != rs.end(); ++it)
{
    const row& row = *it;
    cout << "id:" << row.get<long long>(0)
        << " first_name:" << row.get<string>(1)
        << " last_name:" << row.get<string>(2) << endl;
  
}

这里get模版的参数类型必需和数据库类型一一对应,varchar和text类型对应string,整数类型按如下关系对应

数据库类型 soci类型
SMALLINT int
MEDIUMINT int
INT long long
BIGINT unsigned long long

update

int id = 1;
string first_name = "hello";
string last_name = "world";
sql << "update Person set first_name=:first_name, last_name=:last_name"
    " where id=:id", 
    use(first_name), use(last_name), use(id);

delete

int id = 1;
sql << "delete from Person where id=:id", use(id);

有时候我们需要关注delete操作是否真的删除了数据,mysql本身也会返回操作影响的行数,可以采用如下方法获取

statement st = (sql.prepare << "delete from Person where id=:id", use(id));
st.execute(true);
int affected_rows = st.get_affected_rows();

使用连接池

使用连接池可以解决多线程的问题,每个线程在操作数据库时先从连接池取出一个session,这个session会被设置为锁定,用完之后再换回去,设置为解锁,这样不同线程使用不同session,互不影响。session对象可以用连接池来构造,构造时自动锁定,析构时自动解锁

int g_pool_size = 3;
connection_pool g_pool(g_pool_size);
for (int i = 0; i < g_pool_size; ++i)
{
    session& sql = g_pool.at(i);
    sql.open("mysql", "dbname=test user=zhangmenghan password=123456");
}
session sql(g_pool);
sql << "select * from Person";

此时session sql(g_pool)的调用是没有超时时间的,如果没有可用的session,会一直阻塞,如果要设置超时时间,可以采用connection_pool的底层接口

session & at(std::size_t pos);
bool try_lease(std::size_t & pos, int timeout);
void give_back(std::size_t pos);

调用方式如下

size_t pos
if (!try_lease(pos, 3000)) // 锁定session,设置超时为3秒
{
    return;
}
session& sql = g_pool.at(pos) // 获取session,此时pos对应的session已被锁定
/* sql操作 ... */
g_pool.give_back(pos); // 解锁pos对应的session

需要注意的是,如果try_lease调用成功后没有调用give_back,会一直锁定对应的session,因此try_leasegive_back必需成对使用

事务

session对象提供了对事务的操作方法

void begin();
void commit();
void rollback();

同时也提供了封装好的transaction对象,使用方式如下

{
    transaction tr(sql);

    sql << "insert into ...";
    sql << "more sql queries ...";
    // ...

    tr.commit();
}

如果commit没有被执行,则transaction对象在析构时会自动调用session对象的rollback方法

ORM

soci可以通过自定义对象转换方式从而在use和into语法中直接使用用户对象

比如针对Person表单我们定义如下结构和转换函数

struct Person
{
    uint32_t id;
    string first_name;
    string last_name;
}

namespace soci {
template<>
struct type_conversion<Person>
{
    typedef values base_type;
    static void from_base(const values& v, indicator ind, Person& person)
    {
        person.id = v.get<long long>("id");
        person.first_name = v.get<string>("first_name");
        person.last_name = v.get<string>("last_name");

    }
    static void to_base(const Person& person, values& v, indicator& ind)
    {
        v.set("id", (long long)person.id);
        v.set("first_name", person.first_name);
        v.set("last_name", person.last_name);
    }
};
}

需要注意的是这里get模板的参数类型必需和数据库字段对应,对应关系见之前select的示例,对于整数类型,在set时最好也加上强转并且和get一致,否则可能会抛异常std::bad_cast。get和set函数的第一个参数是占位符,占位符的名字不一定要和数据库column名一致,但后续操作中values语法里的占位符必需和这里指定的一致

定义了type_conversion之后,后续在用到use和into语法时可直接使用Person对象,这时soci会根据占位符操作指定字段

insert

Person person;
person.first_name = "Steve";
person.last_name = "Jobs";
sql << "insert into Person(first_name, last_name)"
    " values(:first_name, :last_name)", use(person);

select

int id = 1;
Person person;
sql << "select * from Person where id=:id", use(id), into(person);

rowset<Person> rs = (sql.prepare << "select * from Person");
for (rowset<Person>::iterator it = rs.begin(); it != rs.end(); ++it)
{
    const Person& person = *it;
    // do something with person
}

update

person.id = 1;
person.first_name = "hello";
person.last_name = "world";
sql << "update Person set first_name=:first_name, last_name=:last_name"
    " where id=:id", use(person);

delete

Person person;
person.id = 1;
sql << "delete from Person where id=:id", use(person);

完整示例

https://github.com/handy1989/soci_test

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容