1. 介绍
- ODB是c++的一个对象-关系映射(ORM)系统。它提供了工具、api和库支持,允许您将c++对象持久化到关系数据库(RDBMS),而无需处理表、列或SQL,也无需手工编写任何映射代码。
- ODB是非常灵活和可定制的。它可以完全隐藏底层数据库的关系性质,也可以根据需要公开一些细节。例如,您可以自动将基本c++类型映射到合适的SQL类型,为持久类生成关系数据库模式,并使用简单、安全但功能强大的对象查询语言来代替SQL。或者,您可以为单个数据成员分配SQL类型,使用现有的数据库模式,运行本机SQL SELECT查询,并调用存储过程。实际上,在极端情况下,ODB可以作为一种方便的方式来处理本机SQL查询的结果。
- ODB不是一个框架。它并没有规定你应该如何编写你的应用程序。相反,它通过只处理对象持久性而不干扰任何其他功能来适应您的风格和体系结构。不存在所有持久类都应该派生的通用基类型,也不存在对持久类中的数据成员类型的任何限制。现有的类可以通过少量修改或不修改而实现持久性。
- ODB被设计为高性能和低内存开销。准备好的语句用于发送和接收二进制格式的对象状态,而不是文本格式,这减少了应用程序和数据库服务器上的负载。对连接、准备好的语句和缓冲区进行大量缓存可以节省建立连接、语句解析和内存分配的时间和资源。对于每个受支持的数据库系统,使用本机C API而不是ODBC或更高级别的包装器API来减少开销,并为每个数据库操作提供最有效的实现。最后,持久化类的内存开销为零,没有每个类必须拥有的隐藏“数据库”成员,也没有ODB分配的每个对象数据结构。
- 在这一章中,我们将对ODB进行高层次的概述。我们将从ODB体系结构开始,然后概述使用ODB构建应用程序的工作流程。我们将继续对比将c++对象保存到关系数据库的传统方法的缺点和使用ODB实现对象持久性的优点。我们通过讨论ODB支持的c++标准来结束本章。下一章将采用更实际的方法,展示在一个简单的“Hello World”应用程序中实现对象持久性所需的具体步骤。 ODB学习笔记之基础环境搭建 https://blog.csdn.net/u011057439/article/details/79260615
1.1 架构和工作流程
从应用程序开发人员的角度来看,ODB由三个主要组件组成 : ODB编译器、公共运行时库(称为libodb)和特定于数据库的运行时库(称为libodb-<dat abase>),其中<database>是该运行时所针对的数据库系统的名称,例如: libodb-mysql。例如,如果应用程序将使用MySQL数据库进行对象持久性,那么该应用程序将使用的三个ODB组件是ODB编译器、libodb 和 libodb-mysql。
ODB编译器为应用程序中的持久类生成数据库支持代码。ODB编译器的输入是一个或多个c++头文件,这些头文件定义了希望持久使用的c++类。对于每个输入头文件,ODB编译器生成一组c++源文件,实现在这个头文件中定义的持久c++类及其数据库表示之间的转换。ODB编译器还可以生成一个数据库模式文件,该文件创建存储持久类所需的表。
ODB编译器是一个真正的c++编译器,只不过它生成的是c++而不是汇编代码或机器码。特别地,它不是一个只能够识别c++子集的特别头预处理器。ODB能够解析任何标准c++代码。公共运行时库定义了独立于数据库系统的接口,应用程序可以使用这些接口操作持久对象。这些接口的特定于数据库的运行时库提供了实现一个具体的数据库以及其他特定于数据库实用工具生成的代码使用的正常情况下,应用程序并不直接使用特定于数据库的运行时库,而是与它从libodb通过公共接口。下面的图表显示了一个使用MysQL作为底层数据库系统的应用程序的对象持久性架构:
ODB系统还定义了两种特殊用途的语言:ODB Pragma语言和ODB查询语言。ODB Pragma语言通过嵌入在c++头文件中的特殊的#pragm a指令将持久化类的各种属性传递给ODB编译器。它控制对象-关系映射的各个方面,比如用于持久类及其成员的表和列的名称,或者c++类型和数据库类型之间的映射.
ODB查询语言是一种面向对象的数据库查询语言,可用于搜索匹配特定条件的对象。它是仿造的,并集成到c++中,允许您编写具有表达性和安全性的查询,这些查询看起来和感觉上就像普通的c++。
使用ODB编译器生成数据库支持代码为应用程序的构建序列增加了额外的步骤。下图概述了使用ODB的应用程序的典型构建流程:
1.2 优点
将c++对象保存到关系数据库的传统方法要求您手工编写代码,在数据库和每个持久化类的c++表示之间进行转换。
这些代码通常执行的操作包括c++值与字符串或数据库类型之间的转换、SQL查询的准备和执行,以及处理结果集。手动编写这段代码有以下缺点:
-
这既困难又费时。为任何重要的应用程序编写数据库转换代码,都需要对特定的数据库系统及其api有广泛的了解。它还需要相当多的时间来编写和维护。支持多线程应用程序会使这个任务更加复杂。
-
表现不佳。最优的转换通常需要编写大量额外的代码,比如用于准备好的语句和缓存连接、语句和缓冲区的参数bindina。以一种特殊的方式编写这样的代码通常太困难,也太耗费时间。
-
数据库厂商锁定。转换代码是为一个特定的数据库,这使得很难切换到其他数据库供应商。
-
缺少类型安全。在SoL gueries中很容易拼错列名或传递不兼容的值。这样的错误只会在运行时被检测到。
-
复杂应用程序。数据库转换代码通常分散在整个应用程序中,使其难以调试、更改和维护。
相比之下,使用ODB实现c++对象持久性有以下好处 :
-
易用性。ODB从c++类声明自动生成数据库转换代码,并允许您使用简单和线程安全的面向对象的数据库api操作持久对象。
-
简洁的代码。由于ODB隐藏了底层数据库的细节,应用程序loaic是使用自然对象词汇表而不是表来编写的。列和SOL,结果代码更简单,因此更容易阅读和理解。
-
最佳性能。ODB被设计为高性能和低内存开销。所有可用的优化技术,如准备语句和扩展连接、语句和缓冲区缓存,都用于为每个数据库操作提供最有效的实现。
-
数据库的可移植性。由于数据库转换代码是自动生成的,所以很容易从一个数据库供应商切换到另一个数据库供应商。事实上,在做出选择之前,可以在几个数据库系统上测试您的应用程序安全。ODB对象持久化和查询api是静态类型的。使用c++标识符而不是字符串来引用对象成员,生成的代码确保数据库和c++类型是兼容的。所有这些都有助于在编译时捕获编程错误,而不是在运行时捕获。
-
可维护性。自动代码生成将使应用程序适应持久类中的更改所需的工作量降至最低。数据库支持代码与类声明和应用程序逻辑是分开的。这使得应用程序更容易调试和维护。
总的来说,ODB为c++提供了一个易于使用的、灵活而强大的对象关系映射(ORM)系统。与c++的其他ORM实现不同的是,它仍然要求您为每个持久化类编写数据库转换或成员注册代码,ODB保持持久化类纯粹是声明性的。功能部分,即数据库转换代码,是由ODB编译器从这些声明自动生成的。
1.3 支持的c++标准
ODB 为 ISO/IEC C++ 1998/2003 (C++98/03)、ISO/IEC TR 19768 提供支持c++库扩展(c++ TR1)和ISO/IEC c++ 2011 (c++ 11)。虽然本手册中的大多数示例使用的是c++ 98/03,但对TR1和c++ 11中引入的新功能和库组件的支持在整个文档中都有讨论。ODB -examples包中的c++11示例还显示了对各种c++11特性的ODB支持。
2 Hello World示例
在本章中,我们将展示如何使用传统的“Hello World”示例创建一个简单的依赖于或ODB的C+应用程序来实现对象持久性。特别是,我们将讨论如何声明持久类、生成数据库支持代码以及编译和运行我们的应用程序。我们还将学习如何使对象具有持久性、加载、更新和删除持久性对象,以及在数据库中查询匹配特定条件的持久性对象。该示例还展示了如何定义和使用视图,视图是一种机制,允许我们创建持久对象的投影、数据库表,或者处理本机sQL查询或存储过程调用的结果。
本章中给出的代码基于hello示例,该示例可以在ODR发行版的odb-examples包中找到。
2.1 声明持久化类
在“Hello World”的例子中,我们会稍微偏离常规,向人们而不是整个世界问好。在我们的应用程序中,person将表示为c++类person的对象,person 保存在 person.hxx 文件中:
// person.hxx
//
#include <string>
class person
{
public:
person (const std::string& first,
const std::string& last,
unsigned short age);
const std::string& first () const;
const std::string& last () const;
unsigned short age () const;
void age (unsigned short);
private:
std::string first_;
std::string last_;
unsigned short age_;
};
为了不错过我们需要问候的任何人,我们希望将person对象保存在数据库中。为了实现这一点,我们将person类声明为persistent:
// person.hxx
//
#include <string>
#include <odb/core.hxx> // (1)
#pragma db object // (2)
class person
{
...
private:
person () {} // (3)
friend class odb::access; // (4)
#pragma db id auto // (5)
unsigned long id_; // (5)
std::string first_;
std::string last_;
unsigned short age_;
};
为了能够将person对象保存在数据库中,我们必须对原始类定义做5个更改,标记为(1)到(5)。
第一个变化是包含了ODB头文件<odb/core.hx>。该头文件提供了许多核心ODB声明,如ODB: access,这些声明用于定义持久类。
第二个更改是在类定义之前添加了db对象pragma。这个pragma告诉ODB编译器,后面的类是持久的。请注意,使类持久并不意味着该类的所有对象将自动存储在数据库中。您仍然可以像以前一样创建这个类的普通或暂时实例。不同的是,现在您可以将这样的瞬态实例持久化,我们将很快看到这一点。
第三个更改是添加了默认构造函数。odb生成的数据库支持代码在从持久状态实例化对象时将使用这个构造函数。就像我们对person类所做的那样,如果不想让类的用户使用默认构造函数,可以将其设为private或protected。还请注意,有一些限制,可以有一个没有默认构造函数的持久化类在
第四个更改中,我们使odb::access类成为person类的朋友。这是使默认构造函数和数据成员可被数据库支持代码访问所必需的。如果您的类有一个公共默认构造函数和公共数据成员,或者数据成员的公共访问器和修饰符,那么友元声明是不必要的。
最后一个更改是添加一个名为id的数据成员,在ODB中,id之前是另一个pragma,每个持久对象通常在其类中有一个唯一的标识符。或。换句话说,同一类型的两个持久实例没有相同的标识符。
虽然可以定义一个持久化类没有种id,可以执行数据库操作的数量是有限的,在这样一个类为我们班我们使用整数id。db id自动编译指示之前id_成员告诉OpB编译器,以下是种的成员标识符。自动指示符表明它是一个数据库分配的id。唯一的id将由数据库自动生成,并在持久化时关联到对象。
在本例中,我们选择添加一个标识符,因为现有的成员都不能满足相同的目的。但是,如果一个类已经有一个具有合适属性的成员。然后很自然地使用该成员作为标识符。例如,如果我们的person类包含某种形式的个人身份(SSN在美国或其他国家的身份证/护照号码),然后我们可以使用它作为一个ID,或者如果我们存储与每个人相关的电子邮件,然后我们可以使用,如果每个人都认为有一个唯一的电子邮件地址。
作为另一个示例,考虑person类的以下替代版本。这里我们使用一个现有的数据成员作为id。此外,数据成员保持私有,而是通过公共访问器和修饰符函数访问。最后,将ODB pragma分组在一起并放在类定义之后。也可以将它们移到单独的头文件中,使原始类完全保持不变(有关这种非侵入式转换的更多信息,请参阅 Chapter 14, "ODB Pragma Language")。
class person
{
public:
person ();
const std::string& email () const;
void email (const std::string&);
const std::string& get_name () const;
std::string& set_name ();
unsigned short getAge () const;
void setAge (unsigned short);
private:
std::string email_;
std::string name_;
unsigned short age_;
};
#pragma db object(person)
#pragma db member(person::email_) id
现在我们有了带有persistent类的头文件,让我们看看如何生成数据库支持代码。
2.2 生成数据库支持代码
我们在上一节中创建的持久化类定义对任何能够实际完成任务并将人的数据存储到数据库的代码都特别轻。没有序列化或反序列化代码,甚至没有数据成员注册,这些代码通常需要在c++的其他ORM库中手工编写。这是因为在ODB代码中,在数据库和c++之间转换对象的表示是由ODB编译器自动生成的。
编辑这个人。hx头文件,我们在上一节创建,并生成MySQL数据库的支持代码,我们从终端UNIX调用或命令提示符(Windows)调用ODB编译器:
db -d mysql -generate-query person.hxx
在本章的剩余部分,我们将使用MySQL作为首选数据库,当然也可以使用其他支持的数据库系统。
如果你没有安装 the common ODB runtime library (libodb
) 或者安装到c++编译器默认不搜索头的目录中,那么你可能会得到以下错误:
person.hxx:10:24: fatal error: odb/core.hxx: No such file or directory
要解决这个问题,你需要用-I预处理器选项指定libodb头文件的位置,例如 :
odb -I.../libodb -d mysql --generate-query person.hxx
在这里 .../libodb 表示 libodb目录的路径。
上面对ODB编译器的调用产生三个c++文件: person-odb.hxx,
person-odb.ixx,
person-odb.cxx
.。通常不直接使用这些文件中包含的类型或函数。相反,您需要做的只是包含person-odb.hxx。在c++文件中的hxx中,您使用 person 中的类执行数据库操作。hxx 以及编译person_odb.cxx,并将生成的对象文件链接到应用程序。
您可能想知道——generate-query选项是用来干什么的。它指示ODB编译器生成可选的查询支持代码,我们将在稍后的“Hello World”示例中使用这些代码。我们会发现另一个有用的选项是——generate-schema。这个选项使ODB编译器生成第四个文件person。sgl,它是person中定义的持久类的数据库模式。hxx:
odb -d mysql --generate-query --generate-schema person.hxx
数据库模式文件包含创建存储持久化类所需的表的SQL语句。我们将在下一节中学习如何使用它。
如果您想查看所有可用ODB编译器选项的列表,请参阅 ODB Compiler Command Line Manual.。
现在我们已经有了持久化类和数据库支持代码,剩下的唯一部分就是应用程序代码,它可以用这些代码做一些有用的事情。但是在我们进入有趣的部分之前,让我们首先了解如何构建和运行使用ODB的应用程序。这样,当我们有一些应用程序代码要尝试时,在我们可以运行它之前就不会有更多的延迟。
2.3 编译与运行
假设mainO函数和应用程序代码保存在驱动程序中。如前一节所述,生成了exx和数据库支持代码和模式,为了构建我们的应用程序,我们首先需要编译所有c++源文件,然后将它们与两个ODB运行时库链接起来
在UNIX上,编译部分可以用以下命令完成(用c++编译器的名称替换ct+;关于Microsoft Visual Studio的设置,请参阅odexample packaqe);
c++ -c driver.cxx
c++ -c person-odb.cxx
与ODB编译类似,如果在ODB /或ODB /mysql目录中没有找到头文件,则需要使用-I预处理器选项来指定公共ODB运行时库(libodb)和MySOL ODE运行时库(libodb-mysgl)的位置。
编译完成后,我们可以用以下命令链接应用程序:
c++ -o driver driver.o person-odb.o -lodb-mysql -lodb
注意,我们将应用程序与两个ODB库连接起来:libodb是一个通用的运行时库,libodb- MySQL是一个MySQL运行时库(如果您使用另一个数据库,那么这个库的名称将相应地改变)。如果出现一个错误,说找不到这些库中的一个,那么需要使用-L链接器选项来指定它们的位置
在运行应用程序之前,我们需要使用生成的person创建数据库模式。于sq1文件。对于mysql,我们可以使用mysql客户端程序,例如:
mysql --user=odb_test --database=odb_test < person.sql
上面的命令将以odb_test用户(不带密码)登录到本地MySQL服务器,并使用名为odb test的数据库。注意,在执行这个命令之后,存储在odb测试数据库中的所有数据都将被删除。
还要注意,使用独立生成的SQL文件并不是在ODB中创建数据库模式的唯一方法。我们还可以将模式直接嵌入到我们的应用程序中,或者使用不是由ODB编译器生成的自定义模式。详细信息请参见3.4节“数据库”。
一旦数据库模式就绪,我们就使用相同的登录名和数据库名运行我们的应用程序:
./driver --user odb_test --database odb_test
2.4 持久化对象
现在我们已经完成了基础设施的工作,现在是时候看看与数据库交互的第一个代码片段了。在本节中,我们将学习如何使person对象持久化:
// driver.cxx
//
#include <memory> // std::auto_ptr
#include <iostream>
#include <odb/database.hxx>
#include <odb/transaction.hxx>
#include <odb/mysql/database.hxx>
#include "person.hxx"
#include "person-odb.hxx"
using namespace std;
using namespace odb::core;
int
main (int argc, char* argv[])
{
try
{
auto_ptr<database> db (new odb::mysql::database (argc, argv));
unsigned long john_id, jane_id, joe_id;
// Create a few persistent person objects.
//
{
person john ("John", "Doe", 33);
person jane ("Jane", "Doe", 32);
person joe ("Joe", "Dirt", 30);
transaction t (db->begin ());
// Make objects persistent and save their ids for later use.
//
john_id = db->persist (john);
jane_id = db->persist (jane);
joe_id = db->persist (joe);
t.commit ();
}
}
catch (const odb::exception& e)
{
cerr << e.what () << endl;
return 1;
}
}
让我们一段一段地检查这段代码。在开始时,我们包含了一组头文件。标准c++头文件之后包含<odb/database。hx >和< odb /事务。hxx>定义了独立于数据库系统的odb::database和odb:: transaction接口。然后包含<odb/ysal/database。其中hx>定义了MySQL实现的数据库接口。最后,我们包括person。hx person-odb。hxx,它定义了我们的持久person类。
然后我们有两个using namespace指令。第一个引入标准名称空间的名称,第二个引入ODB声明,我们稍后将在文件中使用ODB声明。注意,在第二个指令中,我们使用odb::core名称空间,而不仅仅是odb。前者只在当前的命名空间中引入基本的ODB名称,如数据库和事务类,而没有任何辅助对象。这将最小化与其他库的名称冲突的可能性。还请注意,在限定单个名称时,应该继续使用odb名称空间。例如,你应该写odb:: database,而不是odb:: core:: database。
一旦我们进入mainO),我们要做的第一件事就是创建MySOL数据库对象。注意,这是驱动程序中的最后一行。exx明确提到MySQL;其余的代码通过公共接口工作,并且是独立于数据库系统的。我们使用argc/argv mysql:: database构造函数,它自动从命令行提取数据库参数,例如登录名、密码、数据库名等。在你自己的应用程序中,你可能更喜欢使用其他的mysql: database构造函数,它允许你直接传递这个信息(第17.2节,“mysql数据库类”)
接下来,我们创建三个person对象。现在它们是临时对象,这意味着如果我们在此时终止应用程序,它们将消失,而没有任何证据表明它们曾经存在过。下一行开始一个数据库事务。我们将在本手册后面详细讨论事务。现在,我们需要知道的是,所有ODB数据库操作都必须在事务中执行,事务是工作的原子单元;事务中执行的所有数据库操作要么一起成功(提交),要么自动撤消(回滚)
一旦进入事务,我们就在每个person对象上调用persist O database函数。此时,每个对象的状态都保存在数据库中,但是请注意,在提交事务之前,这种状态不是永久的。例如,如果我们的应用程序在此时崩溃,仍然没有证据表明我们的对象曾经存在过
在我们的例子中,当我们调用persist 0时,还会发生另一件事)。对persistto的调用是这个assiment发生的地方。一旦这个函数返回,id成员就包含这个对象的唯一标识符。为了方便起见,persist C函数还返回一个对象标识符的副本,该标识符已被持久化。我们将每个对象的返回标识符保存在一个局部变量中。稍后,我们将在chanter中使用这些标识符来在我们的持久对象上生成其他数据库操作。
在持久化了对象之后,就该提交事务并使更改永久存在了。只有在commit)函数成功返回后,我们才能保证对象是持久的。如果我们的应用程序由于某种原因在提交后终止,那么数据库中对象的状态将保持不变。实际上,我们很快就会发现,我们的应用程序可以重新启动,并从数据库加载原始对象。还要注意,事务必须通过commit调用显式提交。如果事务对象在没有显式提交或回滚事务的情况下离开作用域,则事务对象将自动回滚。这种行为允许您不必担心在事务中抛出异常:如果异常跨越事务边界,事务将自动回滚,对数据库所做的所有更改都将撤消
我们示例中的最后一部分代码是处理数据库异常的catch块。我们通过捕获基本ODB异常(Section 3.14, "ODB Exceptions")并打印诊断结果来实现这一点。
现在让我们编译(Section 2.3, "Compiling and Running"),然后运行我们的第一个ODE应用程序:
mysql --user=odb_test --database=odb_test < person.sql
./driver --user odb_test --database odb_test
我们的第一个应用程序除了错误消息外不打印任何东西,因此我们不能真正地知道它是否实际上在数据库中存储了对象的状态。虽然我们很快就会使我们的应用程序更有趣,但是现在我们可以使用mysal客户机来检查数据库内容。它还可以让我们了解对象是如何存储的:
mysql --user=odb_test --database=odb_test
Welcome to the MySQL monitor.
mysql> select * from person;
+----+-------+------+-----+
| id | first | last | age |
+----+-------+------+-----+
| 1 | John | Doe | 33 |
| 2 | Jane | Doe | 32 |
| 3 | Joe | Dirt | 30 |
+----+-------+------+-----+
3 rows in set (0.00 sec)
mysql> quit
另一种深入了解底层情况的方法是跟踪ODB在每次数据库操作后执行的SQL语句。下面是我们如何在事务的持续时间内启用跟踪:
// Create a few persistent person objects.
//
{
...
transaction t (db->begin ());
t.tracer (stderr_tracer);
// Make objects persistent and save their ids for later use.
//
john_id = db->persist (john);
jane_id = db->persist (jane);
joe_id = db->persist (joe);
t.commit ();
}
通过这样的修改,我们的应用程序现在产生以下输出:
INSERT INTO `person` (`id`,`first`,`last`,`age`) VALUES (?,?,?,?)
INSERT INTO `person` (`id`,`first`,`last`,`age`) VALUES (?,?,?,?)
INSERT INTO `person` (`id`,`first`,`last`,`age`) VALUES (?,?,?,?)
注意,我们看到的是问号而不是实际值,因为ODB使用了准备好的语句并以二进制形式将数据发送到数据库。有关跟踪的更多信息,请参阅 Section 3.13, "Tracing SQL Statement Execution"。在下一节中,我们将看到如何从应用程序中访问持久对象。
2.5 查询数据库对象
到目前为止,我们的应用程序并不类似于典型的“Hello World”示例。除了错误消息外,它不会打印任何东西。让我们改变这一点,教我们的应用程序向数据库中的人打招呼。为了更有趣一点,我们只向30岁以上的人问好
// driver.cxx
//
...
int main (int argc, char* argv[])
{
try
{
...
// Create a few persistent person objects.
//
{
...
}
typedef odb::query<person> query;
typedef odb::result<person> result;
// Say hello to those over 30.
//
{
transaction t (db->begin ());
result r (db->query<person> (query::age > 30));
for (result::iterator i (r.begin ()); i != r.end (); ++i)
{
cout << "Hello, " << i->first () << "!" << endl;
}
t.commit ();
}
}
catch (const odb::exception& e)
{
cerr << e.what () << endl;
return 1;
}
}
我们的应用程序的前半部分与前面一样,为了简洁起见,在上面的清单中用“..”替换。让我们一块一块地检查它的其余部分这两个typedef为两个模板实例化创建了方便的别名,它们将在我们的应用程序中大量使用。第一个是person对象的查询类型,第二个是该auery的结果类型。然后我们获得一个新的事务并调用querv0数据库函数。我们传递了一个查询表达式(query:: age > 30),该表达式将返回的对象限制为年龄大于30的对象。我们还将查询结果保存在一个局部变量中。接下来的几行对结果序列执行一个标准的for循环迭代,为每个返回的人打印hello。然后提交事务,就这样。让我们看看这个应用程序将打印什么:
mysql --user=odb_test --database=odb_test < person.sql
./driver --user odb_test --database odb_test
Hello, John!
Hello, Jane!
这看起来是对的,但是我们如何知道查询实际上使用了数据库,而不是使用早期persist O)调用的一些内存构件呢?测试这一点的一种方法是注释掉应用程序中的第一个事务,并在不重新创建数据库模式的情况下重新运行它。这样,将返回在前一次运行期间持久化的对象。或者,我们可以重新运行相同的应用程序,而不需要重新创建模式,注意我们现在显示了重复的对象:
./driver --user odb_test --database odb_test
Hello, John!
Hello, Jane!
Hello, John!
Hello, Jane!
这里发生的情况是,我们的应用程序的前一次运行持久化了一组person对象,当我们重新运行应用程序时,我们持久化了另一组具有相同名称但不同id的对象。稍后运行查询时,将返回来自两个集合的匹配项。我们可以更改打印“Hello”字符串的行,如下所示来说明这一点
cout << "Hello, " << i->first () << " (" << i->id () << ")!" << endl;
如果我们现在重新运行这个修改过的程序,同样不需要重新创建数据库模式,我们将得到以下输出:
./driver --user odb_test --database odb_test
Hello, John (1)!
Hello, Jane (2)!
Hello, John (4)!
Hello, Jane (5)!
Hello, John (7)!
Hello, Jane (8)!
上面列表中缺少的标识符3、6和9属于这个查询没有选择的“Joe Dirt”对象。
2.6 更新持久化对象
虽然使对象持久,然后使用查询选择其中一些对象是两个有用的操作,但大多数应用程序还需要更改对象的状态,然后使这些更改持久。让我们通过更新刚刚过生日的Joe的年龄来说明这一点:
// driver.cxx
//
...
int
main (int argc, char* argv[])
{
try
{
...
unsigned long john_id, jane_id, joe_id;
// Create a few persistent person objects.
//
{
...
// Save object ids for later use.
//
john_id = john.id ();
jane_id = jane.id ();
joe_id = joe.id ();
}
// Joe Dirt just had a birthday, so update his age.
//
{
transaction t (db->begin ());
auto_ptr<person> joe (db->load<person> (joe_id));
joe->age (joe->age () + 1);
db->update (*joe);
t.commit ();
}
// Say hello to those over 30.
//
{
...
}
}
catch (const odb::exception& e)
{
cerr << e.what () << endl;
return 1;
}
}
新交易的开始和结束与前两次相同。一旦在一个事务中,我们调用loadO数据库函数来用Joe的持久状态实例化一个personor对象。我们传递Joe的对象标识符,这个标识符是我们之前在持久化这个对象时存储的。虽然这里我们使用std:: auto ptr来管理返回的对象,但我们也可以使用另一个智能指针,例如c++ 11中的std::unique ptr或TR1、c++ 11或Boost中的shared ptr。有关对象生命周期管理和智能指针的更多信息, Section 3.3, "Object and View Pointers".
有了实例化的对象之后,我们增加了对象的年龄,并调用update0函数来更新对象在数据库中的状态。事务提交后,更改将永久生效。如果我们现在运行这个应用程序,我们将在输出中看到Joe,因为他现在已经超过30岁了
mysql --user=odb_test --database=odb_test < person.sql
./driver --user odb_test --database odb_test
Hello, John!
Hello, Jane!
Hello, Joe!
如果我们没有乔的标识符呢?也许这个对象在我们的应用程序的另一次运行中,或者由另一个应用程序完全持久。如果我们的数据库中只有一个Joe Dirt,我们可以使用查询功能来提供上述事务的替代实现:
// Joe Dirt just had a birthday, so update his age. An
// alternative implementation without using the object id.
//
{
transaction t (db->begin ());
// Here we know that there can be only one Joe Dirt in our
// database so we use the query_one() shortcut instead of
// manually iterating over the result returned by query().
//
auto_ptr<person> joe (
db->query_one<person> (query::first == "Joe" &&
query::last == "Dirt"));
if (joe.get () != 0)
{
joe->age (joe->age () + 1);
db->update (*joe);
}
t.commit ();
}
2.7 定义和使用视图
假设我们需要收集关于存储在数据库中的人员的一些基本统计信息。比如总人数,以及最小和最大年龄。一种方法是在数据库中查询所有person对象,然后在遍历查询结果时计算该信息。虽然这种方法在只有三个人的数据库中工作得很好,但是如果我们有大量的对象,它的效率就会非常低。虽然从面向对象编程的角度来看,关系数据库在概念上可能不纯粹,但它可以比我们自己在应用程序过程中执行相同的操作更快、更经济地执行一些计算。为了支持这种情况,ODB提供了视图的概念。ODB视图是一个c++类,它包含一个或多个持久对象或数据库表、本机SOL查询执行或存储过程调用的结果的轻量级只读投影。视图的一些常见应用包括loadina(对象或列数据库表中的数据成员子集),执行和处理任意SQL查询的结果,包括聚合查询,以及使用对象关系或自定义连接条件连接多个对象和/或数据库表。你可以在Chapter 10, "Views",中找到更详细的视图描述,下面是我们如何定义person统计视图,它返回关于person对象的基本统计信息:
#pragma db view object(person)
struct person_stat
{
#pragma db column("count(" + person::id_ + ")")
std::size_t count;
#pragma db column("min(" + person::age_ + ")")
unsigned short min_age;
#pragma db column("max(" + person::age_ + ")")
unsigned short max_age;
};
通常,为了获得视图的结果,我们使用与在数据库中查询对象时相同的query0函数。然而,这里我们执行的聚合查询总是只返回一个元素。因此,我们可以使用快捷的query_value C函数,而不是获取结果实例然后遍历它。下面是我们如何使用刚刚创建的视图来加载和打印统计信息:
// Print some statistics about all the people in our database.
//
{
transaction t (db->begin ());
// The result of this query always has exactly one element.
//
person_stat ps (db->query_value<person_stat> ());
cout << "count : " << ps.count << endl
<< "min age: " << ps.min_age << endl
<< "max age: " << ps.max_age << endl;
t.commit ();
}
如果我们现在将person统计视图添加到person。hx头文件,上面的事务到驱动程序。cxx,以及重新编译和重新运行我们的示例,然后我们将在输出中看到以下额外的行:
count : 3
min age: 31
max age: 33
2.8 删除持久化对象
我们将在本章中讨论的最后一个操作是从数据库中删除持久对象。下面的代码片段展示了如何在给定标识符的情况下删除对象:
// John Doe is no longer in our database.
//
{
transaction t (db->begin ());
db->erase<person> (john_id);
t.commit ();
}
为了从数据库中删除John,我们启动一个事务,使用John的对象id调用erase0)数据库函数,并提交事务。事务提交后,删除后的对象不再持久。如果手边没有对象id,可以使用查询来查找和删除对象:
// John Doe is no longer in our database. An alternative
// implementation without using the object id.
//
{
transaction t (db->begin ());
// Here we know that there can be only one John Doe in our
// database so we use the query_one() shortcut again.
//
auto_ptr<person> john (
db->query_one<person> (query::first == "John" &&
query::last == "Doe"));
if (john.get () != 0)
db->erase (*john);
t.commit ();
}
2.9 修改持久化类
当一个暂态c++类的定义被改变时。例如,通过addina或删除数据成员,我们不必担心该类的任何existina实例与新定义不匹配。毕竟,要使类更改有效,我们必须重新启动应用程序,而没有一个瞬态实例能够幸免于此。对于持久类,事情就不那么简单了。由于它们存储在数据库中,因此不会受到应用程序重启的影响,因此我们有了一个新问题:更改持久化类后,现有对象(对应于旧定义)的状态会发生什么变化?处理旧对象的问题(称为数据库模式演变)是一个复杂的问题,ODB为handlina it提供了全面的支持。这种支持在第13章“数据库模式的演变”中有详细介绍,让我们考虑一个简单的例子,它应该让我们对DB在这方面提供的功能有一个感觉假设在使用我们的持久化类的人一段时间后,创建一个数据库包含它的实例数量,我们意识到,对于一些人来说我们还需要存储他们的中间名,如果我们ao iust添加新的数据成员,一切都将好与新数据库。然而,现有的数据库有一个与新的类定义不对应的表,具体地说,生成的数据库支持代码现在期望有一个列来存储中间名,但是这样的列从来没有在旧数据库中创建过。ODB可以自动生成soL语句,使旧数据库与新的类定义匹配。但首先,我们需要通过为对象模型定义一个版本来支持模式演进:
// person.hxx
//
#pragma db model version(1, 1)
class person
{
...
std::string first_;
std::string last_;
unsigned short age_;
};
版本pragma中的第一个数字是基本模型版本。这是我们能够迁移的最低版本。第二个数字是当前的模型版本。因为我们还没有对persistent类做任何更改,所以这两个值都是1。接下来,我们需要重新编译person。hx头文件与ODB编译器,就像我们之前做的:
odb -d mysql --generate-query --generate-schema person.hxx
如果我们现在查看ODB编译器生成的文件列表,我们会注意到一个新文件:person。xml。这是一个changelog文件,ODB编译器在其中跟踪与我们的类更改相对应的数据库更改。注意,这个文件是由ODB编译器自动维护的,我们所要做的就是在重新编译之间保存它。现在,我们准备将中间名添加到person类中。我们还为它指定了一个默认值(空字符串),该值将被分配给ald数据库中的现有对象。注意,我们还增加了当前版本:
// person.hxx
//
#pragma db model version(1, 2)
class person
{
...
std::string first_;
#pragma db default("")
std::string middle_;
std::string last_;
unsigned short age_;
};
如果我们现在重新编译person。hx头文件,我们将看到两个额外生成的文件:person-002-pre。sql和人- 002。sql。这两个文件包含从版本1到版本2的模式迁移语句。与模式创建类似,模式迁移语句也可以嵌入到生成的c++代码中人- 002前。sql和人- 002。sql是前后模式迁移文件。要迁移一个旧的数据库,我们首先执行预迁移文件:
mysql --user=odb_test --database=odb_test < person-002-pre.sql
如果需要,在模式迁移前和迁移后,我们可以运行数据迁移代码。在这个阶段,我们既可以访问旧数据,也可以存储新数据。在本例中,我们不需要任何数据迁移代码,因为我们为所有现有对象的中间名添加了默认值为了完成迁移过程,我们执行post迁移语句
mysql --user=odb_test --database=odb_test < person-002-post.sql
2.10 使用多个数据库
访问多个数据库(即数据存储)只是创建多个表示每个数据库的odb:b>:: database实例的问题。例如:
odb::mysql::database db1 ("john", "secret", "test_db1");
odb::mysql::database db2 ("john", "secret", "test_db2");
一些数据库系统还允许将多个数据库附加到同一个实例上。更有趣的问题是我们如何从同一个应用程序访问多个数据库系统(即数据库实现)。例如,我们的应用程序可能需要将一些对象存储在远程MySQL数据库中,而其他对象存储在loca SQLite文件中。或者,我们的应用程序可能需要能够将其对象存储在用户在运行时选择的数据库系统中。ODB提供了全面的多数据库支持,从与特定数据库系统的紧密集成到能够编写数据库aanostic代码,以及单个数据库系统的动态支持。所有这些方面都将在 Chapter 16, "Multi-Database Support",在本节中,我们将对“Hello World”示例进行扩展,使其能够将数据存储在MySOL或PostqresOL中(ODB支持的其他数据库系统也可以以类似的方式添加),从而了解该功能。addina多数据库支持的第一步是重新编译人员。hxx头文件为附加的数据库系统生成数据库支持代码:
odb --multi-database dynamic -d common -d mysql -d pgsql \
--generate-query --generate-schema person.hxx
——multi-database ODB编译器选项打开了多数据库支持。目前,我们传递给该选项的动态值的含义并不重要,但如果您想了解,请参阅第16章。这个命令的结果是生成了三组文件:person-odb。? x(公共接口;对应于通用数据库),person-odb-mysql。?xx (MySQL支持代码)和person-odb-pgsgl。xx (PostgreSQL支持代码)。还有两个模式文件:person-mysql。sql和person-pgsgl。sgl。唯一需要改变的部分是驱动程序。cxx是我们创建数据库实例的方式。具体地说,这条线:
auto_ptr<database> db (new odb::mysql::database (argc, argv));
现在,我们的示例能够在MysOL或PostaresQL中存储其数据,因此我们需要以某种方式允许调用者指定我们必须使用哪个数据库。为了简单起见,我们将让第一个命令行参数指定我们必须使用的数据库系统,而其他参数将包含特定于数据库的选项,我们将像前面一样传递给odb:: <ab>:: database构造函数。让我们把所有这些loaic放到一个单独的函数中,我们将调用create database 0)。cxx看起来像(其余部分没有改变):
// driver.cxx
//
#include <string>
#include <memory> // std::auto_ptr
#include <iostream>
#include <odb/database.hxx>
#include <odb/transaction.hxx>
#include <odb/mysql/database.hxx>
#include <odb/pgsql/database.hxx>
#include "person.hxx"
#include "person-odb.hxx"
using namespace std;
using namespace odb::core;
auto_ptr<database>
create_database (int argc, char* argv[])
{
auto_ptr<database> r;
if (argc < 2)
{
cerr << "error: database system name expected" << endl;
return r;
}
string db (argv[1]);
if (db == "mysql")
r.reset (new odb::mysql::database (argc, argv));
else if (db == "pgsql")
r.reset (new odb::pgsql::database (argc, argv));
else
cerr << "error: unknown database system " << db << endl;
return r;
}
int
main (int argc, char* argv[])
{
try
{
auto_ptr<database> db (create_database (argc, argv));
if (db.get () == 0)
return 1; // Diagnostics has already been issued.
...
就是这样。剩下的唯一事情就是构建并运行我们的示例:
c++ -c driver.cxx
c++ -c person-odb.cxx
c++ -c person-odb-mysql.cxx
c++ -c person-odb-pgsql.cxx
c++ -o driver driver.o person-odb.o person-odb-mysql.o \
person-odb-pgsql.o -lodb-mysql -lodb-pgsql -lodb
下面是我们如何访问MySQL数据库:
mysql --user=odb_test --database=odb_test < person-mysql.sql
./driver mysql --user odb_test --database odb_test
或者PostgresQL数据库:
psql --user=odb_test --dbname=odb_test -f person-pgsql.sql
./driver pgsql --user odb_test --database odb_test
2.11 总结
本章展示了一个非常简单的应用程序,尽管如此,它执行了所有的核心数据库函数:persistto、query0、load0、update()和erase0。我们还看到,编写一个使用ODB的应用程序涉及以下步骤:1. 在头文件中声明持久类2. 编译这些头文件以生成数据库支持代码。3.将应用程序与生成的代码和两个ODB运行时库链接起来。在这一点上,如果有很多事情看起来不清楚,也不必担心。本章的目的只是给你一个关于如何用ODB持久化c++对象的一般概念。我们将在本手册的其余部分讨论所有细节