原文地址: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 <[email protected]> 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()方法。