原文地址:http://www.webtoolkit.eu/wt/doc/tutorial/dbo/tutorial.html
-------------------------------------------------------------------------------
Wt::Dbo教程
-------------------------------------------------------------------------------
目录
1. 介绍
2. 映射单个类
3. 第一个session
4. 查询对象
5. 更新对象
6. 映射关系
6.1 多对1关系
6.2 多对多关系
6.3 1对1关系
7. 定制映射
7.1 修改和disabling用于主键的“id”字段
7.2 修改和disabling "version"字段
7.3 指定自然的主键
8. 事务和并发
Koen Deforche <koen@emweb.be> 3.1.6, 2010年10月27日
-------------------------------------------------------------------------------
1. 介绍
-------------------------------------------------------------------------------
Wt::Dbo是一个C++的ORM(对象关系映射)库。
这个库是Wt的一部分,为了建构基于数据库的web应用程序,但是也可以独立使用。
这个库在数据库表上提供了基于类的视图, 它使得数据库对象的对象层与数据库在插入、
更新和删除数据库记录时自动同步。C++类映射到数据库表,类的字段映射到表的列,指针
和指针的集合映射到数据库的关系。一个映射类的对象叫做数据库对象(dbo)。查询的结
果被定义为数据库对象,原数据(primitives)或者它们的元组。
映射问题是使用现代C++的方法来解决的。不是基于xml的描述,也不是使用晦涩的庞宏,
映射的定义完全使用C++代码。
在这个教程里,我们将使用我们的方法实现一个blogging例子,与随库一起发布的那个例
子相似。
Tip : 此教程的完整代码在Wt安装目录的examples/feature/dbo中。
-------------------------------------------------------------------------------
2. 映射单个类
-------------------------------------------------------------------------------
我们将从使用Wt::Dbo映射一个User类到相关的user表开始。
Waring : 在这个教程的例子中,我们将Wt::Dbo命名空间化名为dbo
构建这个例子时,你需要链接wtdbo和wtdbosqlite3库。
tutorial1.C代码
-------------------------------------------------------------------------------
#include <Wt/Dbo/Dbo>
#incluce <string>
namespace dbo = Wt::Dbo;
class User {
public:
enum Role {
Visitor = 0,
Admin = 1,
Alien = 42
};
std::string name;
std::string password;
Role role;
int karma;
template<class Action>
void persist(Action& a) {
dbo::field(a, name, "name");
dbo::field(a, password, "password");
dbo::field(a, role, "role");
dbo::field(a, karlma, "karlma");
}
};
-------------------------------------------------------------------------------
这个例子显示了如何定义一个类的持久化支持。定义一个模块成员方法persist(), 它为
类的持久化定义服务。类中的每一个成员,调用Wt::Dbo::filed()将它映射到一个数据表
的列。
正如你所看到的,这个库直接支持标准C++的类型,如int, std::string和enum类型,对于
其它类型,可以通过特化Wt::Dbo::sql_value_traits<T>来支持。
库中定义了许多action,这些action将通过类的persist()方法应用到数据库对象上。这些
action将读、更新或者插入数据库对象,创建数据库结构,或者产生事件结果。
Note:为了简单起见,我们的这个例子使用了public成员。你也可以将数据成员封装成
private,然后提供访问方法(accessor methods)。这种情况下,你需要针对读、写操作
的不同明确的定义持久化方法。
3. 第一个session
由于我们已经为我们的User类定义了映射,我们可以开始一个数据库会话(session),创建
我们的模式(schema)(如果需要的话)并且向数据库增加一个用户。
让我们看看代码怎么做的。
tutorial1.C 续上
-------------------------------------------------------------------------------
void run() {
/*
* 创建一个会话,一般情况下在application启动时做一次就够了
*/
dbo::backend::Sqlite3 sqlite3("blog.db");
dbo::Session session;
session.setConnection(sqlite3);
...
-------------------------------------------------------------------------------
这个Session对象是一个长期存活的对象,它给我们的数据库对象提供了一个访问接口。典
型情况下,你会在一个application会话的存活期内只创建一个session,并且每个用户一
个。Wt::Dbo中的所有对象都不是线程安全的(除了连接池),session对象也不能在会话间
共享。
线程安装的不足并不单单是我们懒惰的结果。这与基于数据库的事务完整性的承诺是一致
的:你将不想看到在一个会话中做的修改,在其修改还没有提交的时候被另一个会话修改
了。(Read-Commited tranction isolation level)。然而,为了允许在会话间共享大部
分对象,将来实现一个copy-on-write策略可能会更好。
一个connection设置到session里,这个connection是用来与数据库通讯的。session只有
在执行事务的时候才会使用connection,因此,实际不需要一个专有的connection。当你
需要多个并发的session时,用一个连接池代替之会更好,这时session也会用一个连接池
的引用来初始化。
Wt::Dbo对数据库的访问加了一个抽象层,当前支持Postgres和Sqlite3做为后端。
tutorial1.C 续上
-------------------------------------------------------------------------------
...
session.mapClass<User>("user");
/*
* 试图创建一个模式(如果存在的话会返回失败).
*/
session.createTables();
...
-------------------------------------------------------------------------------
接下来,我们用mapClass()注册每一个数据库类到session,以表明这个类必须要映射到这
个数据库表。
当然在开发过程中,而且也在最初的部署时,用Wt::Dbo来创建或删除数据库模式是很方便
的。
这会生成下面的SQL:
-------------------------------------------------------------------------------
begin transaction
create table "user" (
"id" integer primar key autoincrement,
"version" integer not null,
"name" text not null,
"password" text not null,
"role" integer not null,
"karlma" integer not null
)
commit transaction
-------------------------------------------------------------------------------
正如你所看到的,在映射c++字段的4列之后,Wt::Dbo还加了另两个列:id和version。id
是用来替代主键的,version是一个基于版本的乐观锁。从Wt 3.1.4以后,Wt::Dbo可以允
许你禁用version字段,并且提供一个任何类型的自然的键来代替替代主键,参考
Customizing the mapping。
最后,我们可以向数据库中增加一个用户。所有的数据库操作都在事务中执行。
tutorial1.C 续上
-------------------------------------------------------------------------------
...
/*
* 一个单元的工作总是在一个事务中发生
*/
dbo::Transaction transaction(session);
User *user = new User();
user->name = "Joe";
user->password = "Secret";
user->role = User::Visitor;
user->karlma = 13;
dbo::ptr<User> userPtr = session.add(user);
transaction.commit();
}
-------------------------------------------------------------------------------
调用Session::add()会向数据库中增加一个对象。这个调用返回一个ptr<Dbo>,这是一个
Dbo类型的数据库对象的引用。这是一个共享指针,它也记录这个被引用的对象的持久状态。
在每一个session时,一个数据库对象最多被加载一次:session会保持与被加载的数据库
对象联系,同时无论何时一个对数据库的查询需要它的时候,它就返回一个已经存在的对
象。当指针这个数据库对象的最后一个指针超出作用域时,数据库对象临时拷贝(在内存
里)也会被删除(除非它被修改了,此种情况下只有这些修改被成功提交到数据库后才删
除对象的临时拷贝)。
session也会保持与那些被修改了的对象、需要刷新到数据库的对象(使用SQL语句)的联
系。在事务提交时,或者在需要维护临时拷贝和数据库拷贝的一致性时(如,查询之前)
时,刷新会自动发生。
这会产生下面的SQL:
-------------------------------------------------------------------------------
begain transaction
insert into "user" ("version", "name", "password", "role", "karlma") values
(?, ?, ?, ?, ?)
commit transaction
-------------------------------------------------------------------------------
所有的SQL语句都会产生一次(每一个连接),然后多次使用之,这样有个好处,就是可以
避免SQL注入的问题,同时也给予了潜在的更好的性能表现。
-------------------------------------------------------------------------------
4. 查询对象
-------------------------------------------------------------------------------
查询数据库有两种方法。单个Dbo类的数据库对象可能用Session::find<Dbo>(condition)
来查询:
tutorial1.C 续
-------------------------------------------------------------------------------
dbo::ptr<User> joe = session.find<User>().where("name = ?").bind("Joe");
std::cerr << "Joe has karma: " << joe->karlma << std::endl;
-------------------------------------------------------------------------------
所有的查询都使用带位置参数的预备语句(prepared statements)。Session::find<T>方法
返回一个Query<ptr<T> >对象。这个查询对象可以通过定义Sql的where, order by和
group by的定义来重新定义查询(search),同时允许用Query::bind()来绑定参数。这种
情况下的查询应该返回一个单一的结果,然后直接转换成一个数据库对象指针。
Note:Wt 3.1.3以后,Query类有了第二个参数BindStrategy,它有两个可能的值,相应的
也就有了两种不同的查询实现方法。
默认的策略是DynamicBinding,允许查询是一个与会话关联的长期存活的对象。它
可以被使用多次。它也允许你通过修改order和limit/offsets来修改查询。
另一外策略是DirectBinding,它基于预备语句(prepared statements),直接传
递绑定参数。这与旧的Query对象相一致。这样的查询只能运行一次,但是它的好处
就是使用相对较少的资源,因为这时的参数值会直接传给后端,而不是保存在查询
对象中。
对数据库的查询是这样的:
-------------------------------------------------------------------------------
select id, version, "name", "password", "role", "karlma"
from "user"
where (name=?)
-------------------------------------------------------------------------------
更通用的查询方法是用Sesion::query<Result>(sql), 它不仅仅支持数据库对象做为查询
结果。与上面的查询相等的是:
tutorial1.C 续
-------------------------------------------------------------------------------
dbo::ptr<User> joe2 = session.query<dbo::ptr<User> >("select u from user u")
.where("name = ?").bind("Joe");
-------------------------------------------------------------------------------
这会产生相似的SQL:
-------------------------------------------------------------------------------
select u.id, u.version, u."name", u."password", u."role", u."karlma"
from user u
where (name = ?)
-------------------------------------------------------------------------------
传入这个方法的sql语句可以是任意的sql语句,它的返回需要要与Result的类型相匹配。
SQL语句的select部分需要重写(像上面的例子中一样),为的是返回查询到的数据库对象
的独立字段。
为了说明Session::query<Result>()可能会返回其它的类型,考虑下面的查询,它会返回
int类型。
tutorial1.C 续
-------------------------------------------------------------------------------
int count = session.query<int>("select count(1) from user")
.where("name = ?").bind("Joe");
-------------------------------------------------------------------------------
上面的查询只返回了唯一的结果,但是查询也可能会返回多个结果。因此,
Session::query<Result>()可能会返回一个dbo::collection<Result>结果。上面的例子中
我们迫使查询返回唯一的结果是为了方便起见。类似的,Session::find<Dbo>()可能返回一
个collection<ptr<Dbo> >或者ptr<_Dbo>。如果要求返回唯一的结果,但是查询找到了多
个结果,会抛出一个NoUniqueResultException异常。
collection<T>是一个与stl兼容的集合,它有迭代器实现了InputIterator的要求。虽然如
此,你只能对返回的结果集合迭代一次。一旦collection被迭代过了,它就不能再使用了。
(但是,除非Query对象是DirectBinding的,这个Query对象还可以使用)。
下面的代码显示了你如何迭代一多个结果集
tutorial1.C 续
-------------------------------------------------------------------------------
typedef dbo::collection< dbo::ptr<User> > Users;
Users users = session.find<User>();
std::cerr << "We have " << users.size() << " users:" << std::endl;
for (Users::const_iterator i = users.begin();
i != users.end();
++i)
std::cerr << " user " << (*i)->name
<< " with karlma of " << (*i)->karlma << std::end;
-------------------------------------------------------------------------------
这段代码将会执行两次数据库查询:一次是在调用collection::size()时,一次是在迭代
返回结果时。
-------------------------------------------------------------------------------
select count(1) from "user"
select id, version, "name", "password", "role", "karlma" from "user"
-------------------------------------------------------------------------------
Warning:查询会用预备语句去执行,并且,如果没有为那个查询预备语句,它就是预备一
个新的语句。这是因为一个预备语句通常是不能重入的,同时如果这个预备语句
已经存在,查询就会使用它,你必须小心的避免在同一时间有两个集合使用同一
个语句。因此,当正在迭代查询结果的时候,你不能再去使用这个查询。所以,
在迭代查询结果之前,将查询结果拷贝到一个标准容器中也许是必要的。在3.1.3
版本之后,并发的使用会被检测到,同时会抛出一个异常说:
A collection for '...' is already in use. Reentrant statement use is
not yet implemented.
我计划在以后的版本里去掉这个限制,到那时会实现在必要时克隆一个预备语句。
5. 更新对象
不像其它的智能指针,ptr<Dbo>是默认只读的,它返回const Dbo*。为了修改数据库对象,
你需要调用ptr::modify()方法,它会返回一个非静态指针。这会标志对象为dirty,并在
修改后同步到数据库。
tutorial1.C 续
-------------------------------------------------------------------------------
dbo::ptr<User> joe = session.find<User>().where("name = ?").bind("Joe");
joe.modify()->karlma++;
joe.modify()->password = "public";
-------------------------------------------------------------------------------
数据库同步不是立即发生的,相反, 他们会被延迟,直到使用ptr<Dbo>::flush()
或者Session::flush()明确的要求其同步,直到执行一个查询,这个查询的结果可能被这
次修改所影响,或者直到这次的事务提交。
前面的代码会产生下面的SQL:
-------------------------------------------------------------------------------
select id, version, "name", "password", "role", "karma"
from "user"
where (name = ?)
update "user" set "version" = ?, "name" = ?, "password" = ?, "role" = ?,
"karlma" = ?
where "id" = ? and "version" = ?
-------------------------------------------------------------------------------
我们已经看到如何使用Session::add(ptr<Dbo>),我们向数据库增加一个新的对象。相反
的操作就是ptr<Dbo>::remove():它会从数据库删除这个对象。
tutorial1.C 续
-------------------------------------------------------------------------------
dbo::ptr<User> joe = session.find<User>().where("name = ?").bind("Joe");
joe.remove();
-------------------------------------------------------------------------------
删除这个对象的,这个临时的对象仍然可以使用,甚至可以再把它增加到数据库中去。
Note: 像modify()一样,add()和remove()操作也会延时同步到数据库,因此,下面的代码
实际上不会对数据库有任何的影响:
tutorial1.C续
-------------------------------------------------------------------------
dbo::ptr<User> silly = session.add(new User());
silly.modify()->name = "Silly";
silly.remove();
-------------------------------------------------------------------------
-------------------------------------------------------------------------------
6. 映射关系
-------------------------------------------------------------------------------
6.1 多对一关系
让我们来给我们的blogging例子增加posts吧,在posts和users之间定义多对一的关系。
在下面的代码中,我们着重在关系定义的语句上。
Many-to-One relation (tutorial2.C)
-------------------------------------------------------------------------------
#incluee <Wt/Dbo/Dbo>
#include <string>
namespace dbo = Wt::Dbo;
class User;
class Post {
public:
...
dbo::ptr<User> user;
template<class Action>
void persist(Action& a) {
...
dbo::belongsTo(a, user, "user");
}
};
class User {
public:
...
dbo::collection<dbo::ptr<Post> > posts;
template<clas Action>
void persist(Action& a) {
...
dbo::hasMany(a, posts, dbo::ManyToOne, "user");
}
};
-------------------------------------------------------------------------------
在多的一方(Many-side),我们加入了一个对user的引用,在persist()方法里,我们调用
belongsTo()。这允许我们引用那个post所属的user。belongsTo的最后一个参数指定了定
义这个关联的数据库列的名字。
在One-side,我们增加了一个posts的集合,在persist()方法里,我们调用了hasMany()。
连接字段必须与对应的belongsTo()方法中使用相同的名字。
如果我们也用Session::mapClass()增加Post类到我们的session里,并且创建了模式,
会产生下面的SQL:
-------------------------------------------------------------------------------
create table "user" (
...
-- 不会影响user表
);
create table "post" (
...
"user_id" bigint,
constraint "fk_post_user" foreign key ("user_id") references "user" ("id")
)
-------------------------------------------------------------------------------
注:这个user_id字段是与关联名”user"相对应的。
在Many-side,你可以读或写ptr来给post设置所属的user。
在One-side中的collection允许我们获取所有关联元素,但是是只读的:插入元素不会有
任何效果,如果想为一个user增加一个post,你必须为post设置这个user,而不是增加这
个post到user的collection里。
例子:
(tutorial2.C 续)
-------------------------------------------------------------------------------
dbo:ptr<Post> post = session.add(new Post());
post.modify()-user = joe;
// 会打印"Joe has 1 post(s)."
std::cerr << "Joe has " << joe->posts.size() << " post(s)." << std::endl;
-------------------------------------------------------------------------------
如你所见,一旦joe被设置为post的user,这个post就会在joe的posts集合里反映出来。
Warning: 这个collection使用预备语句执行。集合将试着共享一个单一的预备语句,但是
预备语句通常是不可重入的。所以在迭代集合时,你要确保不要重入同一个集合(同一
个或另外的对象的)的迭代。因此,在迭代他们之前,将集合拷贝到一个标准容器(如
std::vector)可能是必要的。
我计划在以后的版本里去掉这个限制,到那时会实现在必要时克隆一个预备语句。
6.2 多对多关系
为了演示多对多关系,我们将给我们的blogging例子增加tag,在posts和tags之间定义多
对多关系。在下面的代码里,我们将着重在定义关联的语句上。
多对多关系 (tutorial2.C)
-------------------------------------------------------------------------------
#include <Wt/Dbo/Dbo>
#include <string>
namespace dbo = Wt::Dbo;
class Tag;
class Post {
public:
...
dbo::collection<dbo::ptr<Tag> > tags;
template<class Action>
void persist(Action& a) {
...
dbo::hasMany(a, tags, dbo::ManyToMany, "post_tags");
}
};
class Tag {
public:
...
dbo::collection<dbo::ptr<Post> > posts;
template<class Action>
void persist(Action& a) {
...
dbo::hasMany(a, posts, dbo::ManyToMany, "post_tags");
}
};
-------------------------------------------------------------------------------
如你所愿,在两个类使用了几乎相同的方法来映射关联:它们都有一个关联类的数据库
对象的collection,在persist()方法中我们调用hasMany()。在这个例子里的连接字段将
会与持久化关系的连接表的名字相一致。
用Session::mapClass()将Post类增加到我们的session中时,在创建模式时,我们将会得
到如下的SQL:
-------------------------------------------------------------------------------
create table "post" (
...
-- post表不会受此关联的影响
)
create table "tag" (
...
-- tag表不会受此关联的影响
)
create table "post_tags" (
"post_id" bigint not null,
"tag_id" bigint not null,
primary key ("post_id", "tag_id"),
constraint "fk_post_tags_key1" foreign key ("post_id")
references "post" ("id"),
constraint "fk_post_tags_key2" foreign key ("tag_id")
references "tag" ("id")
)
create index "post_tags_port" on "post_tags" ("post_id")
create index "post_tags_tag" on "post_tags" ("tag_id")
-------------------------------------------------------------------------------
多对多关系中的双方类的collection允许我们访问相关的元素。然而,不像多对一关系,
我们也可以从collection中insert()和erase()项。为了在post和tag中定义一个关联,你
需要在tag的posts集合中增加一个post,或者在post的tags集合中增加一个tag。你不能同
时两个都做!修改会被自动的映射到对应的collection。同样的,想要将post和tag的关联
去掉,你应该在post的tags集合中删除tag,或者在tag的posts集合中删除post,但不能同
时两个都做。
例如:
(tutorial2.C续)
-------------------------------------------------------------------------------
dbo::ptr<Post> post = ...
dbo::ptr<Tag> cooking = session.add(new Tag());
cooking.modify()->name = "Cooking";
post.modify()->tags.insert(cooking);
// 会打印“1 post(s) tagged with Cooking."
std::cerr << cooking->posts.size() << " post(s) tagged with Cooking."
<< std::endl
-------------------------------------------------------------------------------
Warning:上一个warning也同样适合这里。
6.3 一对一关系
当前还不支持一对一的关系,但是可以用多对一关系来模拟,因为它们的模式结构是一样
的。
-------------------------------------------------------------------------------
7. 定制映射
-------------------------------------------------------------------------------
默认情况下,Wt::Dbo将会在每一个映射的表中增加一个自增的代理主键(id)和一个版本
字段(version)。
虽然这些默认项对一个新的项目来说有意义,但是你也可以裁剪一下,让映射与你的已经
存在的数据库模式相适应。
7.1 改变或禁用代理主键"id"字段
想要改变一个映射类的代理主键的字段的名字,或者不用代理主键而是使用一个自然的主
键,你需要特化 Wt::Dbo::dbo_traits<C>。
例如,下面的代码把Post类的主键从id改为了post_id:
改变“id"字段的名字(tutorial3.C)
-------------------------------------------------------------------------------
#include <Wt/Dbo/Dbo>
namespace dbo = Wt::Dbo;
class Post {
public:
...
};
namespace Wt {
namespace Dbo {
template<>
struct dbo_traits<Post> : public dbo_default_traits {
static const char* surrogateIdField() {
return "post_id";
}
};
}
}
-------------------------------------------------------------------------------
7.2 改变或禁用"version"字段
想要改变乐观并发控制版本字段(version)的名字,或者想彻底的禁用一个类的乐观并发
控制,你需要特化 Wt::Dbo::dbo_traits<C>。
例如,下面的代码禁用了Post类的乐观并发控制:
禁用"version"字段名(tutorial4.C)
-------------------------------------------------------------------------------
#include <Wt/Dbo/Dbo>
namespace dbo = Wt::Dbo;
class Post {
public:
...
};
namespace Wt {
namespace Dbo {
template<>
struct dbo_traits<Post> : public dbo_default_traits {
static const char* versionField() {
return 0;
}
};
}
}
-------------------------------------------------------------------------------
7.3 指定一个自然主键
你可能想不使用自增的代理主键,而是使用一个不同的主键。
例如,下面的代码将User表的主键变为string(它的用户名),它映射到一个varchar(20)的字段user_name上。
使用自然键(tutorial5.C)
-------------------------------------------------------------------------------
#include <Wt/Dbo/Dbo>
namespace dbo = Wt::Dbo;
class User {
public:
std::string userId;
template<class Action>
void persist(Action& a) {
dbo::id(a, userId, "user_id", 20);
}
};
namespace Wt {
namespace Dbo {
template<>
struct dbo_traits<User> : public dbo_default_traits {
static IdType invalidId() {
return std::string();
}
static const char* surrogateIdField() { return 0; }
};
}
}
-------------------------------------------------------------------------------
自然主键也可以是一个组合键。
-------------------------------------------------------------------------------
8. 事务和并发
-------------------------------------------------------------------------------
从数据库中读取数据或向数据库中刷新数据都需要有一个活动的事务。Transaction是一个
RIIA(Resource-Initialization-is-Acquisition资源分配即初始化)类 ,它同时在并发
会话之间起到了隔离作用,还为数据库的修改提供操作原子化。
WtDbo库中实现了一个乐观锁,它允许检查(而不是避免)并发的修改。在数据库中当需要
非写锁的时候,这是一种推荐的和广泛使用的以可伸缩的方式处理并发事项的策略方法。
为了检查并发的修改,在每一表中增加了一个version字段,每次修改后都增加值。当执行
一个修改时(例如更新或删除一个对象),要检查数据库记录的version是否与之前从数据
库中读出来的对象的version相同。
Note:事务的隔离级别
库中的乐观锁需要最小的隔离级别,那就是读提交:事务当中的修改只有当其它的会话在
提交时才能看到。这通常是数据库支持的最低级别的隔离(SQLite3是当前默认提供这一
隔离级别的唯一的数据库后端)
Transaction类在逻辑事务的一个轻量级代理:多个Transactoin对象可能被同时实例化(
通常是嵌套式的),他们每一个的提交都会引起逻辑事务的提交。用这种方法,你可以很
容易有保护那些需要修改这个事务对象访问数据库的独立的方法,它将会自动加入到一个
更宽一点的事务中,只要那是可用的。一个事务实际上会延时打开一个数据库的真正的事
务,直到它需要的那一刻才会去打开,因此为了确保一系列的工作做为一个原子而去实例
化一个事务是没有损失的,即使你还不确定经是做了实质性的工作。
事务可以会失败,处理失败的事务是它们的用法的一个完整面。当库检查一个并发的修改
时,会抛出StaleObjectException异常。也可能抛出其它的异常,包括后端驱动的异常,
例如当数据库的模式与映射不兼容时。也有可能会检测到业务逻辑的问题,它也可能抛出
一个异常而引起事务的回滚,被修改的数据库对象实际上并没有同步到数据库,但是可能
会在之后的一个新的事务中会同步。
显然,很多异常都是致命的。然而,一个值得关注的异常是StaleObjectException。处理
这个异常可能需要不同的策略。不管用什么方法,你至少需要在一个新的事务中提交修改
之前执行这个stale数据库对象的reread()方法。