使用持久对象
前面的章节为我们提供了ODB的高级概述,并展示了如何使用它在数据库中存储C++对象。在本章中,我们将更详细地研究ODB对象持久性模型以及核心数据库API。我们将从第1节和第3节中的基本概念和术语开始,并继续讨论第4节中的odb::数据库类、第5节中的事务和第6节中的连接。本章的其余部分将讨论核心数据库操作,并在最后讨论ODB异常。
在本章中,我们将继续使用和扩展我们在上一章中开发的person持久类。
1.概念和术语
-
数据库一词可以指三个不同的东西:应用程序存储数据的地方的一般概念,用于管理这些数据的软件实现(例如MySQL),最后,一些数据库软件实现可能管理多个通常按名称区分的数据存储。这个名字通常也被称为数据库。
-
在本手册中,当我们使用数据库一词时,我们指的是上面的第一个含义,例如,“
update()
函数将对象的状态保存到数据库中。”数据库管理系统(DBMS)一词通常用于指代数据库一词的第二个含义。在本手册中,我们将简称为数据库系统,例如“独立于数据库系统的应用程序代码”。最后,为了将第三个含义与其他两个含义区分开来,我们将使用数据库名称,例如“第二个选项指定应用程序应用于存储其数据的数据库名称。” -
在C++中,只有一个类型概念和一个类型实例。例如,基本类型(如int)在很大程度上被视为用户定义的类类型。然而,在持久性方面,我们必须对可以存储在数据库中的某些C++类型施加某些限制和要求。因此,我们将持久C++类型分为两组:对象类型和值类型。对象类型的实例称为对象,而值类型的实例则称为值。
-
对象是一个独立的实体。它可以在数据库中独立于其他对象进行存储、更新和删除。通常,对象有一个标识符,称为对象id,在数据库中对象类型的所有实例中都是唯一的。相比之下,值只能作为对象的一部分存储在数据库中,并且没有自己的唯一标识符。
-
一个对象由数据成员组成,这些数据成员要么是值(“值类型”),要么是指向其他对象的指针(“关系”),或者是值的容器,要么是指针指向其他对象(“容器”)。指向其他对象和容器的指针可以被视为特殊类型的值,因为它们也只能作为对象的一部分存储在数据库中。
-
对象类型是一个C++类。由于这种一对一的关系,我们将交替使用对象类型和对象类这两个术语。相比之下,值类型可以是基本的C++类型,如int或类类型,如std::string。如果一个值由其他值组成,则称为复合值及其类型——复合值类型(“复合值类型”)。否则,该值称为简单值及其类型——简单值类型(“简单值类型”)。请注意,简单值和复合值之间的区别是概念性的,而不是代表性的。例如,std::string是一种简单的值类型,因为从概念上讲,string是一个单一的值,即使string类的表示可能包含多个数据成员,每个数据成员都可以被视为一个值。事实上,不同的应用程序可以将相同的值类型视为简单值类型和复合值类型。
-
虽然在纯面向对象的应用程序中不是严格必要的,但实际考虑通常要求我们只加载对象数据成员的子集或来自多个对象的成员组合。我们可能还需要将一些计算分解到关系数据库中,而不是在应用程序的过程中执行它们。为了支持这些要求,ODB区分了第三种C++类型,称为视图(“视图”)。ODB视图是一个C++类,它体现了一个或多个持久对象或数据库表或本机SQL查询执行结果的轻量级只读投影。
-
理解所有这些概念如何映射到关系模型,有望使这些区别更加清晰。在关系数据库中,对象类型映射到表,值类型映射到一个或多个列。简单值类型被映射到一列,而复合值类型则被映射到几列。对象作为一行存储在此表中,值作为一个或多个单元格存储在此行中。一个简单值存储在一个单元格中,而一个复合值占用多个单元格。视图不是持久实体,也不存储在数据库中。相反,它是一种用于捕获SQL查询结果单行的数据结构。
-
回到简单值和复合值之间的区别,考虑一个有三个整数成员的日期类型:年、月和日。在一个应用程序中,它可以被视为一个复合值,每个成员将在关系数据库中获得自己的列。在另一个应用程序中,它可以被视为一个简单的值,并存储在单个列中,作为距离某个预定义日期的天数。
-
到目前为止,我们一直使用术语持久类来指代对象类。我们将继续这样做,即使值类型也可以是类。这种不对称的原因是,在数据库操作中,值类型具有从属性质。请记住,值从不直接存储,而是作为包含它们的对象的一部分存储。因此,当我们说要使C++类持久化或在数据库中持久化类的实例时,我们总是引用对象类而不是值类。
-
通常,您会使用对象类型来建模现实世界中的实体,即具有自己身份的事物。例如,在上一章中,我们创建了一个person类来建模一个人,这是一个真实世界的实体。我们在person类中用作数据成员的姓名和年龄显然是值。很难想象31岁或“Joe”这个名字有自己的身份。
-
确定某物是物体还是价值的一个很好的测试是考虑其他物体是否可能引用它。一个人显然是物体,因为它可以被配偶、雇主或银行等其他物体引用。另一方面,一个人的年龄或名字不是其他物体通常会提及的。
-
此外,当一个对象代表一个真实的实体时,很容易选择一个合适的对象id。例如,对于一个人来说,有一个既定的标识符概念(SSN、学生id、护照号码等)。另一种选择是使用一个人的电子邮件地址作为标识符。
-
但请注意,这些只是指导方针。可能有充分的理由将通常是值的东西变成对象。例如,考虑一个存储大量人员的数据库。此数据库中的许多person对象具有相同的名称和姓氏,将它们存储在每个对象中的开销可能会对性能产生负面影响。在这种情况下,我们可以将名字和姓氏分别设置为对象,并在person类中仅存储指向这些对象的指针。
-
持久类的实例可以处于两种状态之一:瞬态和持久。瞬态实例在应用程序的内存中只有一个表示,当应用程序终止时,它将不复存在,除非它被明确地持久化。换句话说,持久类的瞬态实例的行为就像任何普通C++类的实例一样。持久实例在应用程序的内存和数据库中都有表示。持久实例即使在应用程序终止后也会保留,除非它被明确地从数据库中删除。
2.声明持久对象和值
为了使C++类成为持久对象类,我们使用db对象pragma声明它,例如:
#pragma db object
class person
{
...
};
- 我们经常使用的另一个语法是
db id
,它将一个数据成员指定为对象id,例如:
#pragma db object
class person
{
...
#pragma db id
unsigned long id_;
};
-
对象id可以是简单或复合(“复合对象id”)值类型。此类型应为默认可构造、可复制可构造和可复制可分配。也可以声明一个没有对象id的持久类,但是,这样的类的功能有限(“no_id”)。
-
上述两个语法是声明具有对象id的持久类所需的最低要求。其他语法可用于微调类及其成员的数据库相关属性(“ODB语法语言”)。
-
通常,持久类应该定义默认构造函数。生成的数据库支持代码在从持久状态实例化对象时使用此构造函数。如果我们只为数据库支持代码添加默认构造函数,那么我们可以将其设置为私有,前提是我们还将
<odb/core.hxx>
标头中定义的odb::access
类设置为该对象类的朋友。例如:
#include <odb/core.hxx>
#pragma db object
class person
{
...
private:
friend class odb::access;
person () {}
};
-
也可以有一个没有默认构造函数的对象类。然而,在这种情况下,数据库操作只能将持久状态加载到现有实例中(“加载持久对象”,“查询结果”)。
-
ODB编译器还需要访问持久类的非瞬态(“瞬态”)数据成员。如果这些数据成员是公共的,ODB编译器可以直接访问它们。如果它们是私有的或受保护的,并且
odb::access
类被声明为对象类型的朋友,它也可以这样做。例如:
#include <odb/core.hxx>
#pragma db object
class person
{
...
private:
friend class odb::access;
person () {}
#pragma db id
unsigned long id_;
std::string name_;
};
- 如果数据成员不能直接访问,则ODB编译器将尝试自动找到合适的访问器和修饰符函数。为了实现这一点,ODB编译器将尝试查找从数据成员名称派生的通用访问器和修饰符名称。具体来说,对于上述示例中的
name_data
成员,ODB编译器将查找具有以下名称的访问器函数:get_name()
、getName()
、get name()
和just name()
,以及具有以下名称(set_name()
、setName()
,setName()
)和just-name()
)的修饰符函数。您还可以使用--accessor
正则表达式和--modifier
正则表达式ODB编译器选项添加对自定义名称派生的支持。有关这些选项的详细信息,请参阅《ODB编译器命令行手册》。以下示例说明了自动访问器和修饰符发现:
#pragma db object
class person
{
public:
person () {}
...
unsigned long id () const;
void id (unsigned long);
const std::string& get_name () const;
std::string& set_name ();
private:
#pragma db id
unsigned long id_; // Uses id() for access.
std::string name_; // Uses get_name()/set_name() for access.
};
-
最后,如果数据成员不能直接访问,并且ODB编译器无法发现合适的访问器和修饰符函数,那么我们可以使用db-get和db-set语法提供自定义访问器和修改器表达式。
-
持久类的数据成员也可以拆分为单独加载和/或单独更新的部分
-
您可能想知道我们是否也必须将值类型声明为持久值。对于
int
或std::string
等简单值类型,我们不需要做任何特殊的事情,因为ODB编译器知道如何将它们映射到合适的数据库类型,以及如何在两者之间进行转换。另一方面,如果ODB编译器不知道一个简单的值,那么我们需要提供数据库类型的映射,并可能提供在两者之间转换的代码。 -
与对象类类似,复合值类型必须使用
db-value pragma
显式声明为持久值,例如:
#pragma db value
class name
{
...
std::string first_;
std::string last_;
};
- 请注意,复合值不能将数据成员指定为对象
id
,因为正如我们前面讨论的那样,值没有标识的概念。复合值类型也不必定义默认构造函数,除非它被用作容器的元素。ODB编译器使用与对象类型相同的机制来访问复合值类型中的数据成员。
3.对象和视图指针
-
正如我们在上一章中看到的,一些数据库操作创建了动态分配的持久类实例,并返回指向这些实例的指针。虽然在大多数情况下,您不需要处理指向视图的指针,但可以使用
result_iterator::load()
函数获取动态分配的视图实例(“查询结果”)。 -
默认情况下,所有这些机制都使用原始指针来返回对象和视图,以及传递和缓存对象。对于具有简单对象生存期要求且不使用会话或对象关系的应用程序,这通常就足够了。特别是,从数据库操作中作为原始指针返回的动态分配对象或视图可以分配给我们选择的智能指针,例如C++11的
std::auto_ptr
、std::unique_ptr
,或TR1、C++11或Boost的shared_ptr
。 -
但是,为了避免任何错误的可能性,例如忘记为返回的对象或视图使用智能指针,以及简化更高级的ODB功能的使用,如会话和双向对象关系,建议您使用具有共享语义的智能指针作为对象指针。TR1、C++11或Boost中的
shared_ptr
智能指针是一个很好的默认选择。但是,如果不需要共享并且不使用会话,那么也可以使用std::unique_ptr
或std::auto_ptr
。 -
ODB提供了几种更改对象或视图指针类型的机制。为了在每个对象或每个视图的基础上指定指针类型,我们可以使用db指针语法,例如:
#pragma db object pointer(std::tr1::shared_ptr)
class person
{
...
};
- 我们还可以在命名空间级别为一组对象或视图指定默认指针:
#pragma db namespace pointer(std::tr1::shared_ptr)
namespace accounting
{
#pragma db object
class employee
{
...
};
#pragma db object
class employer
{
...
};
}
- 最后,我们可以使用
--defaultpointer
选项为整个文件指定默认指针。有关此选项参数的详细信息,请参阅《ODB编译器命令行手册》。典型用法如下:
--default-pointer std::tr1::shared_ptr
- 另一种具有相同效果的方法是为全局命名空间指定默认指针:
#pragma db namespace() pointer(std::tr1::shared_ptr)
- 请注意,我们始终可以使用db指针对象或视图pragma覆盖在命名空间级别或使用命令行选项指定的默认指针。例如:
#pragma db object pointer(std::shared_ptr)
namespace accounting
{
#pragma db object
class employee
{
...
};
#pragma db object pointer(std::unique_ptr)
class employer
{
...
};
}
- ODB运行库提供的内置支持允许我们使用
shared_ptr
(TR1或C++11)、std::unique_ptr
(C++11)或std::auto_ptr
作为指针类型。此外,ODB配置文件库可用于常用的框架和库(如Boost和Qt),为这些框架和库中的智能指针提供支持(第三部分,“配置文件”)。
4.数据库
- 在应用程序可以使用ODB提供的持久性服务之前,它必须创建一个数据库类实例。数据库实例是应用程序存储其持久对象的位置的表示。我们通过实例化一个特定于数据库系统的类来创建数据库实例。例如,
odb::mysql::database
就是mysql数据库系统的一个类。我们通常还会将数据库名称作为参数传递给类的构造函数。以下代码片段显示了如何为MySQL数据库系统创建数据库实例:
#include <odb/database.hxx>
#include <odb/mysql/database.hxx>
auto_ptr<odb::database> db (
new odb::mysql::database (
"test_user" // database login name
"test_password" // database password
"test_database" // database name
));
-
odb::database
类是odb提供的所有数据库系统特定类的通用接口。您通常会通过此接口使用数据库实例,除非您的应用程序依赖于特定的功能,并且该功能仅由特定系统的数据库类公开。您需要包含<odb/database.hxx>
头文件,以便在您的应用程序中使用此类。 -
odb::database
接口定义了启动事务和操纵持久对象的函数。本章的其余部分以及下一章将详细讨论这些内容,下一章专门讨论在数据库中查询持久对象的主题。有关系统特定数据库类的详细信息,请参阅第二部分“数据库系统”。 -
在持久化对象之前,必须在数据库中创建相应的数据库模式。该模式包含表定义和其他关系数据库工件,用于存储数据库中持久对象的状态。
-
有几种方法可以创建数据库模式。最简单的方法是指示ODB编译器从持久类生成相应的模式(
--generateschema
选项)。ODB编译器可以将模式生成为独立的SQL文件,嵌入到生成的C++代码中,也可以生成为单独的C++源文件(--schema format
选项)。如果我们使用SQL文件创建数据库模式,那么应该在应用程序启动之前执行此文件,通常只执行一次。 -
或者,如果模式直接嵌入到生成的代码中或作为单独的C++源文件生成,那么我们可以使用
odb::schema_catalog
类在应用程序中的数据库中创建它,例如:
#include <odb/schema-catalog.hxx>
odb::transaction t (db->begin ());
odb::schema_catalog::create_schema (*db);
t.commit ();
-
有关
odb::transaction
的信息,请参阅下一节。上述代码片段的完整版本可以在odb-examples
中的schema/embedded
示例中找到。 -
odb::schema_catalog
类具有以下接口。您需要包含<odb/schemacatalog.hxx>
头文件,以便在您的应用程序中使用此类。
namespace odb
{
class schema_catalog
{
public:
static void
create_schema (database&,
const std::string& name = "",
bool drop = true);
static void
drop_schema (database&, const std::string& name = "");
static bool
exists (database_id, const std::string& name = "");
static bool
exists (const database&, const std::string& name = "")
};
}
-
create_schema()
函数的第一个参数是我们要在其中创建模式的数据库实例。第二个参数是模式名称。默认情况下,ODB编译器使用默认模式名称(空字符串)生成所有嵌入式模式。但是,如果您的应用程序需要有几个单独的模式,您可以使用--schema name
ODB编译器选项来分配自定义模式名称,然后将这些名称用作create_schema()
的第二个参数。默认情况下,create_schema()
还将删除所有数据库对象(表、索引等),如果它们在创建新对象之前存在的话。您可以通过将false作为第三个参数传递来更改此行为。drop_schema()
函数允许您删除所有数据库对象,而无需创建新对象。 -
如果找不到schema,
create_schema()
和drop_schema()
函数将抛出odb::unknown_schema
异常。您可以使用exists()
函数检查目录中是否存在指定数据库和指定名称的架构。还要注意,应该在事务中调用create_schema()
和drop_schema()
函数。 -
ODB还为数据库模式演化提供支持。与模式创建类似,模式迁移语句可以作为独立的SQL文件生成,也可以嵌入到生成的C++代码中。
-
最后,我们还可以在ODB中使用自定义数据库模式。这种方法可以与上述独立SQL文件类似地工作,除了数据库模式是手写的或由另一个程序生成的。或者,我们可以执行自定义SQL语句,直接从我们的应用程序创建模式。为了将持久类映射到自定义数据库模式,ODB提供了广泛的映射自定义语法,如db表、db列和db类型(“ODB语法语言”)。有关如何对各种C++构造执行此类映射的示例代码,请参阅
odb-examples
中的schema/custom
示例。
5.事务
-
事务是原子、一致、隔离和持久(ACID)的工作单元。数据库操作只能在事务中执行,应用程序中的每个执行线程一次只能有一个活动事务。
-
原子性是指在事务中对数据库状态进行更改时,要么应用所有更改,要么根本不应用任何更改。例如,考虑一个在代表银行账户的两个对象之间转移资金的交易。如果第一个对象的借记功能成功,但第二个对象的贷记功能失败,则事务将回滚,第一个对象上的数据库状态保持不变。
-
一致性是指事务必须将存储在数据库中的所有对象从一个一致的状态转换为另一个状态。例如,如果银行账户对象必须引用个人对象作为其所有者,而我们在使对象持久化之前忘记设置此引用,则事务将被回滚,数据库将保持不变。
-
隔离是指在事务期间对数据库状态所做的更改仅在事务内部可见,直到提交为止。使用上述银行转账示例,在信用操作成功完成并提交交易之前,对第一个对象执行的借记操作的结果对其他交易不可见。
-
持久性是指一旦事务被提交,它对数据库状态所做的更改是永久性的,并且可以在应用程序崩溃等故障中幸存下来。从现在开始,更改此状态的唯一方法是执行并提交另一个事务。
-
事务通过调用
database::begin()
或connection::begin()
函数来启动。返回的事务句柄存储在odb::transaction
类的一个实例中。您需要包含<odb/transaction.hxx>
头文件,以便在您的应用程序中使用此类。例如:
#include <odb/transaction.hxx>
transaction t (db.begin ())
// Perform database operations.
t.commit ();
The odb::transaction class has the following interface:
namespace odb
{
class transaction
{
public:
typedef odb::database database_type;
typedef odb::connection connection_type;
explicit
transaction (transaction_impl*, bool make_current = true);
transaction ();
void
reset (transaction_impl*, bool make_current = true);
void
commit ();
void
rollback ();
database_type&
database ();
connection_type&
connection ();
bool
finilized () const;
public:
static bool
has_current ();
static transaction&
current ();
static void
current (transaction&);
static bool
reset_current ();
// Callback API.
//
public:
...
};
}
-
commit()
函数提交一个事务,rollback()
将其回滚。除非事务已经完成,即显式提交或回滚,否则当事务实例超出范围时,事务类的析构函数将自动回滚它。如果我们尝试提交或回滚已完成的事务,则会抛出odb::transaction_already_finalized
异常。 -
database()
访问器返回此事务正在处理的数据库。同样,connection()
访问程序返回此事务所在的数据库连接(第6节,“连接”)。 -
静态
current()
访问器返回此线程的当前活动事务。如果没有活动事务,此函数将抛出odb::not_in_transaction
异常。我们可以使用has_current()
静态函数检查此线程中是否有事务生效。 -
transaction构造函数中的
make_current
参数以及静态current()
修饰符和reset_current()
函数为我们提供了对当前活动事务提名的额外控制。如果我们传递false作为make_current
参数,那么新创建的事务将不会自动成为此线程的活动事务。稍后,我们可以使用static current()
修饰符将此事务设置为活动事务。reset_current()
静态函数清除当前活动的事务。这些机制共同允许更高级的用例,例如在同一线程上复用两个或多个事务。例如:
transaction t1 (db1.begin ()); // Active transaction.
transaction t2 (db2.begin (), false); // Not active.
// Perform database operations on db1.
transaction::current (t2); // Deactivate t1, activate t2.
// Perform database operations on db2.
transaction::current (t1); // Switch back to t1.
// Perform some more database operations on db1.
t1.commit ();
transaction::current (t2); // Switch to t2.
// Perform some more database operations on db2.
t2.commit ();
reset()
修饰符允许我们重用同一事务实例来完成多个数据库事务。与析构函数类似,如果当前事务尚未完成,reset()
将回滚当前事务。默认事务构造函数创建一个已完成的事务,稍后可以使用reset()
对其进行初始化。finished()
访问器可用于检查事务是否已完成。以下是我们如何使用此功能提交当前事务,并在每次执行一定数量的数据库操作时启动新事务:
transaction t (db.begin ());
for (size_t i (0); i < n; ++i)
{
// Perform a database operation, such as persist an object.
// Commit the current transaction and start a new one after
// every 100 operations.
//
if (i % 100 == 0)
{
t.commit ();
t.reset (db.begin ());
}
}
t.commit ();
- 请注意,在上面关于原子性、一致性、隔离性和持久性的讨论中,所有这些保证仅适用于数据库中对象的状态,而不是应用程序内存中对象的状况。可以回滚一个事务,但应用程序内存中仍有该事务的更改。避免这种潜在不一致性的一个简单方法是仅在事务范围内实例化持久对象。例如,考虑同一事务的这两个实现:
void
update_age (database& db, person& p)
{
transaction t (db.begin ());
p.age (p.age () + 1);
db.update (p);
t.commit ();
}
- 在上述实现中,如果
update()
调用失败并且事务回滚,则数据库中person对象的状态和应用程序内存中同一对象的状态将不同。现在考虑一个替代实现,它只在事务期间实例化person对象:
void
update_age (database& db, unsigned long id)
{
transaction t (db.begin ());
auto_ptr<person> p (db.load<person> (id));
p.age (p.age () + 1);
db.update (p);
t.commit ();
}
- 当然,可能并不总是能够以这种风格编写应用程序。我们经常需要访问和修改事务中持久对象的应用程序状态。在这种情况下,如果事务被回滚并且数据库状态保持不变,那么尝试回滚对应用程序状态所做的更改可能是有意义的。一种方法是从数据库中重新加载对象的状态,例如:
void
update_age (database& db, person& p)
{
try
{
transaction t (db.begin ());
p.age (p.age () + 1);
db.update (p);
t.commit ();
}
catch (...)
{
transaction t (db.begin ());
db.load (p.id (), p);
t.commit ();
throw;
}
}
6.连接
odb::connection
类表示与数据库的连接。通常,您不会直接使用连接,而是让ODB运行时根据需要获取和释放连接。但是,某些用例可能需要手动获取连接。为了完整起见,本节描述了连接类并讨论了它的一些用例。如果你是第一次阅读本手册,你可能想跳过这一节。
-
与
odb::database
类似,odb::connection
类是odb提供的所有数据库系统特定类的通用接口。有关系统特定连接类的详细信息,请参阅第二部分“数据库系统”。 -
要使
odb::connection
类在您的应用程序中可用,您需要包含<odb/connection.hxx>
头文件。odb::connection
类具有以下接口:
namespace odb
{
class connection
{
public:
typedef odb::database database_type;
transaction
begin () = 0;
unsigned long long
execute (const char* statement);
unsigned long long
execute (const std::string& statement);
unsigned long long
execute (const char* statement, std::size_t length);
database_type&
database ();
};
typedef details::shared_ptr<connection> connection_ptr;
}
begin()
函数用于在连接上启动事务。execute()
函数允许我们在连接上执行本机数据库语句。它们的语义等同于database::execute()
函数(第12节,“执行本机SQL语句”),除了它们可以在事务外部合法调用。最后,database()
访问器返回对此连接对应的odb::database
实例的引用。
为了获得连接,我们调用database::connection()
函数。连接以odb::connection_ptr
的形式返回,这是一个具有共享指针语义的特定于实现的智能指针。这尤其意味着可以从函数中复制并返回连接指针。一旦指向同一连接的connection_ptr
的最后一个实例被销毁,该连接将返回给数据库实例。以下代码片段展示了如何获取、使用和释放连接:
using namespace odb::core;
database& db = ...
connection_ptr c (db.connection ());
// Temporarily disable foreign key constraints.
//
c->execute ("SET FOREIGN_KEY_CHECKS = 0");
// Start a transaction on this connection.
//
transaction t (c->begin ());
...
t.commit ();
// Restore foreign key constraints.
//
c->execute ("SET FOREIGN_KEY_CHECKS = 1");
// When 'c' goes out of scope, the connection is returned to 'db'.
- 可能需要直接操纵连接的一些用例包括事务外语句执行,例如连接配置语句的执行、每个线程连接策略的实现,以及确保在同一连接上执行一组事务。
7.错误处理和恢复
ODB使用C++异常来报告数据库操作错误。大多数ODB异常表示硬错误或没有应用程序干预就无法纠正的错误。例如,如果我们尝试加载一个对象id未知的对象,则会抛出
odb::object_not_persistent
异常。我们的应用程序可能能够纠正此错误,例如,通过获取有效的对象id并重试。本章其余部分将介绍每个数据库函数可能引发的硬错误和相应的ODB异常,第14节“ODB异常”为所有ODB异常提供了快速参考。
-
第二组ODB异常表示软错误或可恢复错误。此类错误是暂时的故障,通常可以通过简单地重新执行事务来纠正。ODB定义了三个这样的异常:
ODB::connection_lost
、ODB::timeout
和odb::deadlock
。所有可恢复的ODB异常都源自通用的ODB::recovery base
异常,该异常可用于通过单个catch块处理所有可恢复情况。 -
如果在事务处理过程中丢失了与数据库的连接,则抛出
odb::connection_list
异常。在这种情况下,事务被中止,但可以在不做任何更改的情况下重新尝试。同样,如果其中一个数据库操作或整个事务超时,则抛出odb::timeout
异常。同样,在这种情况下,事务被中止,但可以按原样重新尝试。 -
如果两个或多个事务访问或修改多个对象,并且由不同的应用程序或同一应用程序中的不同线程并发执行,那么这些事务可能会试图以不兼容的顺序访问对象并导致死锁。死锁的典型示例是两个事务,其中第一个事务修改了object1,并等待第二个事务将其更改提交给object2,以便它也可以更新object2。同时,第二个事务已经修改了object2,并且正在等待第一个事务将其更改提交给object1,因为它还需要修改object1。因此,这两项交易都无法完成。
-
数据库系统检测到这种情况,并在其中一个死锁事务中自动中止等待操作。在ODB中,这转换为从其中一个数据库函数抛出
odb::deadlock
可恢复异常。 -
以下代码片段显示了如何通过重新启动受影响的事务来处理可恢复的异常:
const unsigned short max_retries = 5;
for (unsigned short retry_count (0); ; retry_count++)
{
try
{
transaction t (db.begin ());
...
t.commit ();
break;
}
catch (const odb::recoverable& e)
{
if (retry_count > max_retries)
throw retry_limit_exceeded (e.what ());
else
continue;
}
}
8.使对象持久化
- 新创建的持久类实例是暂时的。我们使用
database::persist()
函数模板使瞬态实例持久化。此函数有四个重载版本,具有以下签名:
template <typename T>
typename object_traits<T>::id_type
persist (const T& object);
template <typename T>
typename object_traits<T>::id_type
persist (const object_traits<T>::const_pointer_type& object);
template <typename T>
typename object_traits<T>::id_type
persist (T& object);
template <typename T>
typename object_traits<T>::id_type
persist (const object_traits<T>::pointer_type& object);
-
在这里和本手册的其余部分中,
object_traits<T>::pointer_type
和object_traits<T>::const_pointer_type
分别表示不受限制的和恒定的对象指针类型(第3节,“对象和视图指针”)。同样,object_traits<T>::id_type
表示对象id类型。odb::object_traits
模板是odb编译器生成的数据库支持代码的一部分。 -
第一个persist()函数需要一个对被持久化实例的常量引用。第二个函数需要一个常量对象指针。这两个函数只能用于具有应用程序分配的对象ID的对象。
-
第三个和第四个persist()函数与前两个类似,除了它们对不受限制的引用和对象指针进行操作。如果数据库分配了要持久化的对象的标识符,则这些函数会用分配的值更新传递实例的id成员。所有四个函数都返回新持久化对象的对象id。
-
如果数据库中已经包含具有此标识符的此类型的对象,则persist()函数将抛出
odb::object_already_persistent
异常。只要持久的对象数量不超过id类型的值空间,数据库分配的对象id就永远不会发生这种情况。 -
在调用
persist()
函数时,我们不需要显式指定模板类型,因为它将从传递的参数中自动推断出来。以下示例显示了如何调用这些函数:
person john ("John", "Doe", 33);
shared_ptr<person> jane (new person ("Jane", "Doe", 32));
transaction t (db.begin ());
db.persist (john);
unsigned long jane_id (db.persist (jane));
t.commit ();
cerr << "Jane's id: " << jane_id << endl;
-
请注意,在上述代码片段中,我们创建了计划在启动事务之前使其持久化的实例。同样,我们在提交交易后打印了Jane的id。一般来说,您应该避免在事务范围内执行可以在事务开始之前或终止之后执行的操作。活动事务既消耗应用程序的资源,如数据库连接,也消耗数据库服务器的资源,例如对象锁。通过遵循上述规则,您可以确保这些资源被释放,并尽快提供给应用程序中的其他线程和其他应用程序。
-
一些数据库系统支持通过执行单个底层语句来持久化多个对象,这可以显著提高性能。对于这样的数据库系统,ODB提供了批量
persist()
函数。
9.加载持久对象
- 一旦一个对象被持久化,并且你知道它的对象id,应用程序就可以使用database::load()函数模板加载它。此函数有两个重载版本,具有以下签名:
template <typename T>
typename object_traits<T>::pointer_type
load (const typename object_traits<T>::id_type& id);
template <typename T>
void
load (const typename object_traits<T>::id_type& id, T& object);
- 给定一个对象id,第一个函数在动态内存中分配一个对象类的新实例,从数据库加载其状态,并返回指向新实例的指针。第二个函数将对象的状态加载到现有实例中。如果数据库中没有具有此id的此类型的对象,则这两个函数都会抛出
odb::object_not_persistent
。
当我们调用第一个load()
函数时,我们需要显式指定对象类型。我们不需要对第二个函数这样做,因为对象类型将从第二个参数自动推断出来,例如:
transaction t (db.begin ());
auto_ptr<person> jane (db.load<person> (jane_id));
db.load (jane_id, *jane);
t.commit ();
- 在某些情况下,可能需要从数据库中重新加载对象的状态。虽然使用第二个
load()
函数很容易实现,但ODB提供了具有许多特殊属性的database::reload()
函数模板。此函数有两个重载版本,具有以下签名:
template <typename T>
void
reload (T& object);
template <typename T>
void
reload (const object_traits<T>::pointer_type& object);
- 第一个
reload()
函数需要对象引用,而第二个函数需要对象指针。这两个函数都希望传递的对象中的id成员包含有效的对象标识符,并且与load()
类似,如果数据库中没有具有此id的此类型的对象,这两个功能都将抛出odb::object_not_persistent
。
与load()
函数相比,reload()
的第一个特殊属性是它不与会话的对象缓存交互(“对象缓存”)。也就是说,如果重新加载的对象已经在缓存中,那么在reload()
返回后,它将留在那里。同样,如果对象不在缓存中,则reload()
也不会将其放在那里。
-
reload()
函数的第二个特殊属性仅在对具有乐观并发模型的对象进行操作时才显示出来。在这种情况下,如果应用程序内存和数据库中对象的状态相同,则不会发生重新加载。 -
如果我们不确定具有给定id的对象是否持久,我们可以使用
find()
函数而不是load()
,例如:
template <typename T>
typename object_traits<T>::pointer_type
find (const typename object_traits<T>::id_type& id);
template <typename T>
bool
find (const typename object_traits<T>::id_type& id, T& object);
-
如果在数据库中找不到具有此id的对象,则第一个
find()
函数将返回NULL指针,而第二个函数将传递的实例保持不变并返回false。 -
如果我们不知道对象id,那么我们可以使用查询来查找符合某些条件的对象(或多个对象)(“查询数据库”)。但是请注意,使用对象的标识符加载对象的状态可能比执行查询快得多。
10.更新持久对象
- 如果持久对象已被修改,我们可以使用
database::update()
函数模板将更新后的状态存储在数据库中。此函数有三个重载版本,具有以下签名:
template <typename T>
void
update (const T& object);
template <typename T>
void
update (const object_traits<T>::const_pointer_type& object);
template <typename T>
void
update (const object_traits<T>::pointer_type& object);
- 第一个
update()
函数需要一个对象引用,而另外两个函数需要对象指针。如果传递给这些函数之一的对象在数据库中不存在,update()
会抛出odb::object_not_persistent
异常(但请参阅下面关于乐观并发性的说明)。
下面是我们在前面的交易部分中讨论的资金转账示例。它使用假设的bank_account
持久类:
void
transfer (database& db,
unsigned long from_acc,
unsigned long to_acc,
unsigned int amount)
{
bank_account from, to;
transaction t (db.begin ());
db.load (from_acc, from);
if (from.balance () < amount)
throw insufficient_funds ();
db.load (to_acc, to);
to.balance (to.balance () + amount);
from.balance (from.balance () - amount);
db.update (to);
db.update (from);
t.commit ();
}
- 使用动态分配的对象和带有对象指针参数的
update()
函数也可以实现同样的功能,例如:
transaction t (db.begin ());
shared_ptr<bank_account> from (db.load<bank_account> (from_acc));
if (from->balance () < amount)
throw insufficient_funds ();
shared_ptr<bank_account> to (db.load<bank_account> (to_acc));
to->balance (to->balance () + amount);
from->balance (from->balance () - amount);
db.update (to);
db.update (from);
t.commit ();
-
如果任何
update()
函数都在乐观并发模型的持久类上运行,那么如果数据库中对象的状态自上次加载到应用程序内存以来发生了变化,它们将抛出odb::object_changed
异常。此外,对于此类类,如果数据库中没有此类对象,update()
将不再抛出object_not_persistent异常。相反,此条件被视为对象状态的更改,并抛出object_chang
。 -
在ODB中,持久类、复合值类型以及单个数据成员都可以声明为只读(“只读(对象)”、“只读的(复合值)”和“只读”(数据成员))。
-
如果一个单独的数据成员被声明为只读,那么在使用上述任何
update()
函数更新对象的数据库状态时,对该成员的任何更改都将被忽略。const
数据成员会自动被视为只读。如果复合值被声明为只读,则其所有数据成员都被视为只读。 -
如果整个对象被声明为只读,则无法更改此对象的数据库状态。对这样的对象调用上述任何
update()
函数都会导致编译时错误。 -
与
persist()
类似,对于支持此功能的数据库系统,ODB提供批量update()
函数。
11.删除持久对象
- 要从数据库中删除持久对象的状态,我们使用
database::erase()
或database::erase_query()
函数模板。如果应用程序仍然有已擦除对象的实例,则此实例将变为瞬态。erase()
函数有以下重载版本:
template <typename T>
void
erase (const T& object);
template <typename T>
void
erase (const object_traits<T>::const_pointer_type& object);
template <typename T>
void
erase (const object_traits<T>::pointer_type& object);
template <typename T>
void
erase (const typename object_traits<T>::id_type& id);
-
第一个
erase()
函数使用对象引用形式的对象本身从数据库中删除其状态。接下来的两个函数实现了相同的结果,但使用了对象指针。请注意,这三个函数都不会改变传递的对象。它只是变得短暂。最后一个函数使用对象id来标识要删除的对象。如果数据库中不存在该对象,则所有四个函数都会抛出odb::object_not_persistent
异常(但请参阅下面关于乐观并发的注释)。 -
在调用最后一个
erase()
函数时,我们必须指定对象类型。对于前三个函数来说,这是不必要的,因为对象类型将从它们的参数中自动推断出来。以下示例显示了如何调用这些函数:
person& john = ...
shared_ptr<jane> jane = ...
unsigned long joe_id = ...
transaction t (db.begin ());
db.erase (john);
db.erase (jane);
db.erase<person> (joe_id);
t.commit ();
-
如果除最后一个函数外的任何
erase()
函数都在乐观并发模型的持久类上运行,那么如果数据库中的对象状态自上次加载到应用程序内存以来发生了变化,它们将抛出odb::object_changed
异常。此外,对于这样的类,如果数据库中没有这样的对象,erase()
将不再抛出object_not_persistent
异常。相反,此条件被视为对象状态的更改,并抛出object_chang
。 -
与
persist()
和update()
类似,对于支持此功能的数据库系统,ODB提供批量erase()
函数。
erase_query()
函数允许我们删除符合特定条件的多个对象的状态。它使用查询表达式database::query()
(“查询数据库”),并且由于ODB查询功能是可选的,因此只有指定了--generate-query
ODB编译器选项,它才可用。erase_query()
函数有以下重载版本:
template <typename T>
unsigned long long
erase_query ();
template <typename T>
unsigned long long
erase_query (const odb::query<T>&);
- 第一个
erase_query()
函数用于删除数据库中存储的给定类型的所有持久对象的状态。第二个函数使用传递的查询实例仅删除符合查询条件的对象的状态。这两个函数都返回已删除的对象数量。调用erase_query()
函数时,我们必须显式指定要擦除的对象类型。例如:
typedef odb::query<person> query;
transaction t (db.begin ());
db.erase_query<person> (query::last == "Doe" && query::age < 30);
t.commit ();
- 与
query()
函数不同,在调用erase_query()
时,我们不能使用查询表达式中指向对象的成员。然而,我们仍然可以将指针对应的成员用作具有所指向对象的id类型的普通对象成员(“关系”)。这允许我们比较对象ID以及测试指针是否为NULL。例如,以下事务确保引用即将删除的雇主对象的所有员工对象也被删除。这里我们假设employee
类包含一个指向employer
类的指针。
typedef odb::query<employee> query;
transaction t (db.begin ());
employer& e = ... // Employer object to be deleted.
db.erase_query<employee> (query::employer == e.id ());
db.erase (e);
t.commit ();
12.执行本机SQL语句
- 在某些情况下,我们可能需要执行本机SQL语句,而不是使用上面描述的面向对象数据库API。例如,我们可能希望调整ODB编译器生成的数据库模式,或者利用我们使用的数据库系统特有的功能。
database::execute()
函数有三个重载版本,提供以下功能:
unsigned long long
execute (const char* statement);
unsigned long long
execute (const std::string& statement);
unsigned long long
execute (const char* statement, std::size_t length)
- 第一个
execute()
函数预期SQL语句为以零结尾的C字符串。最后一个版本预期显式语句长度为第二个参数,语句本身可能包含“\0”字符,例如,如果数据库系统支持的话,表示二进制数据。所有三个函数都返回受语句影响的行数。例如:
transaction t (db.begin ());
db.execute ("DROP TABLE test");
db.execute ("CREATE TABLE test (n INT PRIMARY KEY)");
t.commit ();
- 虽然这些函数必须始终在事务内调用,但可能需要在事务外执行本机语句。这可以使用第6节“连接”中描述的
connection::execute()
函数来完成。
13.跟踪SQL语句执行
-
通常,了解高级数据库操作执行了哪些SQL语句是有用的。例如,我们可以使用这些信息来找出为什么某些交易没有产生预期的结果,或者为什么它们需要比预期更长的时间。
-
虽然这些信息通常可以从数据库日志中获得,但ODB提供了一种更方便、更细粒度的应用程序端SQL语句跟踪支持。例如,在需要跟踪的典型情况下,我们希望看到SQL语句作为特定事务的结果执行。虽然从数据库日志中提取这样的语句子集可能很困难,但使用ODB跟踪支持很容易实现:
transaction t (db.begin ());
t.tracer (stderr_tracer);
...
t.commit ();
- ODB允许我们在数据库、连接和事务级别指定跟踪器。如果为数据库指定,则将跟踪在此数据库上执行的所有语句。另一方面,如果为连接指定了跟踪器,则只会跟踪在此连接上执行的SQL语句。同样,为事务指定的跟踪器将仅显示作为此事务的一部分执行的语句。所有三个类(
odb::database
,odb::connection
,and odb::transaction
)都提供相同的跟踪API:
void
tracer (odb::tracer&);
void
tracer (odb::tracer*);
odb::tracer*
tracer () const;
-
前两个
tracer()
函数允许我们设置跟踪器对象,第二个函数允许我们通过传递NULL指针来清除当前跟踪器。最后一个tracer()
函数允许我们获取当前的tracer对象。如果没有有效的跟踪程序,它将返回一个NULL指针。请注意,跟踪API不管理跟踪程序对象的生存期。示踪剂应在使用期间有效。此外,跟踪API不是线程安全的。试图同时从多个线程设置跟踪器将导致未定义的行为。 -
odb::tracer
类定义了一个回调接口,可用于创建自定义跟踪器实现。odb::stderr_tracer
和odb::stderror_full_tracer
是odb运行时提供的内置跟踪器实现。它们都将正在执行的SQL语句打印到标准错误流中。完整的跟踪器除了跟踪语句执行外,还跟踪它们的准备和释放。完整跟踪器特别有用的一种情况是,语句(例如自定义查询)包含语法错误。在这种情况下,错误将在准备过程中被检测到,因此,语句将永远不会被执行。查看此类语句的唯一方法是使用完整跟踪。 -
odb::tracer
类在<odb/tracer.hxx>
头文件中定义,为了使该类在您的应用程序中可用,您需要将其包含在内。odb::tracer
接口提供了以下回调函数:
namespace odb
{
class tracer
{
public:
virtual void
prepare (connection&, const statement&);
virtual void
execute (connection&, const statement&);
virtual void
execute (connection&, const char* statement) = 0;
virtual void
deallocate (connection&, const statement&);
};
}
-
prepare()
和releaseback()
函数分别在创建和销毁prepare语句时被调用。第一个execute()
函数在执行预处理语句时调用,而第二个函数在执行普通语句时调用。prepare()
和releaseplace()
函数的默认实现什么也不做,而第一个execute()
函数调用第二个函数,将语句文本作为第二个参数传递。因此,如果你只关心正在执行的SQL语句,那么你只需要重写第二个execute()
函数。 -
除了常见的
odb::tracer
接口外,每个数据库运行时还提供一个特定于数据库的版本,如odb::<database>::tracer
。它的接口与通用版本完全相同,除了连接和语句类型是特定于数据库的,这使我们可以访问额外的特定于数据库信息。
例如,考虑一个更复杂的、特定于PostgreSQL的跟踪器实现。在这里,我们依赖于这样一个事实,即PostgreSQL ODB运行时使用名称来标识准备好的语句,并且可以从odb::pgsql::statement
对象中获得此信息:
#include <odb/pgsql/tracer.hxx>
#include <odb/pgsql/database.hxx>
#include <odb/pgsql/connection.hxx>
#include <odb/pgsql/statement.hxx>
class pgsql_tracer: public odb::pgsql::tracer
{
virtual void
prepare (odb::pgsql::connection& c, const odb::pgsql::statement& s)
{
cerr << c.database ().db () << ": PREPARE " << s.name ()
<< " AS " << s.text () << endl;
}
virtual void
execute (odb::pgsql::connection& c, const odb::pgsql::statement& s)
{
cerr << c.database ().db () << ": EXECUTE " << s.name () << endl;
}
virtual void
execute (odb::pgsql::connection& c, const char* statement)
{
cerr << c.database ().db () << ": " << statement << endl;
}
virtual void
deallocate (odb::pgsql::connection& c, const odb::pgsql::statement& s)
{
cerr << c.database ().db () << ": DEALLOCATE " << s.name () << endl;
}
};
- 还请注意,您只能使用数据库特定的数据库实例设置数据库特定的跟踪对象,例如:
pgsql_tracer tracer;
odb::database& db = ...;
db.tracer (tracer); // Compile error.
odb::pgsql::database& db = ...;
db.tracer (tracer); // Ok.
14.ODB异常情况
-
在前面的部分中,我们已经提到了数据库函数可能引发的一些异常。在本节中,我们将讨论ODB异常层次结构,并记录公共ODB运行时可能引发的所有异常。
-
ODB异常层次结构的根是抽象的
ODB::exception
类。此类派生自std::exception
,并具有以下接口:
namespace odb
{
struct exception: std::exception
{
virtual const char*
what () const throw () = 0;
};
}
-
捕获此异常保证我们将捕获ODB抛出的所有异常。
what()
函数返回触发异常的条件的可读描述。 -
ODB可能抛出的具体异常如下表所示:
namespace odb
{
struct null_pointer: exception
{
virtual const char*
what () const throw ();
};
// Transaction exceptions.
//
struct already_in_transaction: exception
{
virtual const char*
what () const throw ();
};
struct not_in_transaction: exception
{
virtual const char*
what () const throw ();
};
struct transaction_already_finalized: exception
{
virtual const char*
what () const throw ();
};
// Session exceptions.
//
struct already_in_session: exception
{
virtual const char*
what () const throw ();
};
struct not_in_session: exception
{
virtual const char*
what () const throw ();
};
struct session_required: exception
{
virtual const char*
what () const throw ();
};
// Database operations exceptions.
//
struct recoverable: exception
{
};
struct connection_lost: recoverable
{
virtual const char*
what () const throw ();
};
struct timeout: recoverable
{
virtual const char*
what () const throw ();
};
struct deadlock: recoverable
{
virtual const char*
what () const throw ();
};
struct object_not_persistent: exception
{
virtual const char*
what () const throw ();
};
struct object_already_persistent: exception
{
virtual const char*
what () const throw ();
};
struct object_changed: exception
{
virtual const char*
what () const throw ();
};
struct result_not_cached: exception
{
virtual const char*
what () const throw ();
};
struct database_exception: exception
{
};
// Polymorphism support exceptions.
//
struct abstract_class: exception
{
virtual const char*
what () const throw ();
};
struct no_type_info: exception
{
virtual const char*
what () const throw ();
};
// Prepared query support exceptions.
//
struct prepared_already_cached: exception
{
const char*
name () const;
virtual const char*
what () const throw ();
};
struct prepared_type_mismatch: exception
{
const char*
name () const;
virtual const char*
what () const throw ();
};
// Schema catalog exceptions.
//
struct unknown_schema: exception
{
const std::string&
name () const;
virtual const char*
what () const throw ();
};
struct unknown_schema_version: exception
{
schema_version
version () const;
virtual const char*
what () const throw ();
};
// Section exceptions.
//
struct section_not_loaded: exception
{
virtual const char*
what () const throw ();
};
struct section_not_in_object: exception
{
virtual const char*
what () const throw ();
};
// Bulk operation exceptions.
//
struct multiple_exceptions: exception
{
...
virtual const char*
what () const throw ();
};
}
-
当指向用
db not_null
或db value_not_null
pragma声明为非null的持久对象的指针具有null值时,会抛出null_pointer
异常。 -
接下来的三个异常(
already_in_transaction
、not_in_transaction
和transaction_already_finalized
)由odb::transaction
抛出,并在第5节“事务”中进行了讨论。 -
接下来的两个异常(
already_in_session
和not_in_session
)由odb::session
类抛出。 -
当ODB检测到正确加载双向对象关系需要会话但会话未被使用时,会抛出
session_required
异常。 -
可恢复异常是所有可恢复异常的共同基础,这些异常是:
connection_lost
、timeout
和deadlock
。当与数据库的连接丢失时,会抛出connection_lost
异常。同样,如果其中一个数据库操作或整个事务超时,则会抛出超时异常。当数据库系统检测到事务死锁时,会抛出死锁异常。任何数据库函数都可以抛出这些异常。详见第7节“错误处理和恢复”。 -
object_already_persistent
异常是由persist()
数据库函数抛出的。有关详细信息,请参阅第8节“使对象持久化”。 -
object_not_persistent
异常是由load()
、update()
和erase()
数据库函数抛出的。有关更多信息,请参阅第9节“加载持久对象”、第10节“更新持久对象”和第11节“删除持久对象”。 -
当对具有乐观并发模型的对象进行操作时,
update()
数据库函数和某些erase()
数据库功能会抛出object_changed
异常。 -
查询结果类引发
result_not_cached
异常。 -
database_exception
异常是数据库系统特定运行时库抛出的所有数据库系统特定异常的基类。 -
当我们试图持久化、更新、加载或删除多态抽象类的实例时,数据库函数会抛出
abstract_class
异常。有关抽象类的更多信息, -
当我们试图持久化、更新、加载或擦除应用程序中不存在类型信息的多态类的实例时,数据库函数会抛出
no_type_info
异常。这通常意味着为此类生成的数据库支持代码尚未链接(或动态加载)到应用程序中,或者鉴别器值尚未映射到持久类。 -
如果已经缓存了具有指定名称的准备好的查询,则
cache_query()
函数会抛出prepared_already_cached
异常。如果指定的准备好的查询对象类型或参数类型与缓存中的类型不匹配,则lookup_query()
函数会抛出prepared_type_mismatch
异常。 -
如果找不到具有指定名称的模式,
odb::schema_catalog
类将抛出未知模式异常。详见第4节“数据库”。如果传递的版本未知,则处理数据库模式演变的schema_catalog
函数会抛出unknow_schema_version
异常。 -
如果我们试图更新尚未加载的对象节,则会抛出
section_not_looaded
异常。如果加载或更新的节实例不属于相应的对象,则抛出section_not_in_object
异常。 -
multiple_exceptions
异常由批量API函数引发。 -
odb::exception
类在<odb/exception.hxx>
头文件中定义。所有具体的ODB异常都在<ODB/exceptions.hxx>
中定义,其中还包括<ODB/exception.hxx>
。通常,您不需要包含这两个标头中的任何一个,因为它们是由<odb/database.hxx>
自动包含的。但是,如果处理ODB异常的源文件不包含<ODB/database.hxx>
,则需要显式包含其中一个标头。