【开源库学习】libodb库学习(一)

Hello World Example

在本章中,我们将使用传统的"Hello World"示例展示如何创建一个依赖于ODB进行对象持久化的简单C++应用程序。特别是,我们将讨论如何声明持久类、生成数据库支持代码以及编译和运行我们的应用程序。我们还将学习如何使对象持久化,加载、更新和删除持久化对象,以及在数据库中查询符合特定条件的持久化对象。该示例还展示了如何定义和使用视图,这是一种允许我们创建持久化对象、数据库表的投影,或处理本机SQL查询或存储过程调用结果的机制。

本章中介绍的代码基于hello示例,该示例可以在odb-examples中找到。

1.声明持久类

  • 首先有一个表示Person的类
cpp 复制代码
// 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类声明为持久类:
cpp 复制代码
// 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对象保存在数据库中,我们必须对原始类定义进行五次更改,标记为(1)到(5)。第一个更改是包含了ODB标头<ODB/core.hxx>。此标头提供了许多核心ODB声明,例如ODB::access,用于定义持久类。
  • 第二个变化是在类定义之前添加了db对象pragma。这个杂注告诉ODB编译器后面的类是持久的。请注意,使类持久化并不意味着该类的所有对象都会自动存储在数据库中。您仍然可以像以前一样创建此类的普通或临时实例。不同之处在于,现在您可以使这种瞬态实例持久化,我们稍后会看到。
  • 第三个变化是添加了默认构造函数。当从持久状态实例化对象时,ODB生成的数据库支持代码将使用此构造函数。正如我们为person类所做的那样,如果你不想让类的用户使用默认构造函数,你可以将其设置为私有或受保护。另请注意,有一些限制,可以在没有默认构造函数的情况下拥有持久类。
  • 通过第四个更改,我们使odb::access类成为person类的友元类。这是使数据库支持代码可以访问默认构造函数和数据成员所必需的。如果你的类有一个公共的默认构造函数,以及公共数据成员或数据成员的公共访问器和修饰符,那么朋友声明是不必要的。
  • 最后的更改添加了一个名为id_的数据成员,前面有另一个pragma。在ODB中,每个持久对象在其类中通常都有一个唯一的标识符。或者,换句话说,同一类型的两个持久实例没有相同的标识符。虽然可以定义一个没有对象id的持久类,但可以在这样的类上执行的数据库操作的数量是有限的。对于我们的类,我们使用一个整数id。id_member之前的db-id-auto杂注告诉ODB编译器以下成员是对象的标识符。自动说明符表示它是数据库分配的id。当对象被持久化时,数据库将自动生成一个唯一的id并将其分配给对象。
  • 在这个例子中,我们选择添加一个标识符,因为现有的成员都不能达到同样的目的。但是,如果一个类已经有一个具有合适属性的成员,那么使用该成员作为标识符是很自然的。例如,如果我们的人员类别包含某种形式的个人身份信息(美国的SSN或其他国家的ID/护照号码),那么我们可以将其用作ID。或者,如果我们存储了与每个人相关的电子邮件,那么如果假设每个人都有一个唯一的电子邮件地址,我们就可以使用它。
  • 再举一个例子,考虑以下person类的替代版本。在这里,我们使用现有的数据成员之一作为id。此外,数据成员保持私有,而是通过公共访问器和修饰符函数进行访问。最后,ODB语法被分组在一起,并放置在类定义之后。它们也可以被移动到一个单独的标头中,使原始类完全保持不变。
cpp 复制代码
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

2. 生成数据库支持代码

我们在上一节中创建的持久类定义对于任何可以实际完成这项工作并将人的数据存储到数据库的代码来说都特别轻。在C++的其他ORM库中,没有序列化或反序列化代码,甚至没有数据成员注册,你通常必须手工编写。这是因为在数据库和C++对象表示之间转换的ODB代码是由ODB编译器自动生成的。

  • 为了编译我们在上一节中创建的person.hxx标头并生成MySQL数据库的支持代码,我们从终端(UNIX)或命令提示符(Windows)调用ODB编译器:
bash 复制代码
odb -d mysql --generate-query person.hxx
  • 在本章的其余部分,我们将使用MySQL作为首选数据库,但也可以使用其他受支持的数据库系统。
    如果您没有安装公共ODB运行时库(libodb),或者将其安装到C++编译器默认不搜索头文件的目录中,那么您可能会收到以下错误:
bash 复制代码
person.hxx:10:24: fatal error: odb/core.hxx: No such file or directory
  • 要解决这个问题,您需要使用-I预处理器选项指定libodb标头位置,例如:
bash 复制代码
odb -I.../libodb -d mysql --generate-query person.hxx
  • 这里/libodb表示libodb目录的路径。
  • 上述对ODB编译器的调用产生了三个C++文件:person-ODB.hxx、person-ODB.ixx和person-ODB.cxx。您通常不会直接使用这些文件中包含的类型或函数。相反,您所要做的就是在C++文件中包含person-odb.hxx,您可以在其中使用person.hxx中的类执行数据库操作,编译person-odb.cxx并将生成的对象文件链接到您的应用程序。
  • 您可能想知道--generate查询选项的用途。它指示ODB编译器生成可选的查询支持代码,我们稍后将在"Hello World"示例中使用这些代码。我们会发现另一个有用的选项是------生成模式。此选项使ODB编译器生成第四个文件person.sql,它是person.hxx中定义的持久类的数据库模式:
bash 复制代码
odb -d mysql --generate-query --generate-schema person.hxx
  • 数据库模式文件包含创建存储持久类所需的表的SQL语句。
  • 如果您想查看所有可用ODB编译器选项的列表,请参阅ODB Compiler Command Line Manual
  • 现在我们有了持久类和数据库支持代码,剩下的唯一部分就是应用程序代码,它对所有这些都有用。

3. 编译和运行

假设main()函数和应用程序代码保存在driver.cxx中,并且数据库支持代码和模式如前一节所述生成,为了构建我们的应用程序,我们首先需要编译所有的C++源文件,然后将它们与两个ODB运行时库链接。

在UNIX上,编译部分可以用以下命令完成(用c++编译器名称替换c++;有关Microsoft Visual Studio设置,请参阅odb示例包):

bash 复制代码
c++ -c driver.cxx
c++ -c person-odb.cxx
  • 与ODB编译类似,如果您收到一个错误,说明在ODB/或ODB/mysql目录中找不到标头,则需要使用-I预处理器选项指定公共ODB运行时库(libodb)和mysql ODB运行时库(libodb-mysql)的位置。
  • 编译完成后,我们可以使用以下命令链接应用程序:
bash 复制代码
c++ -o driver driver.o person-odb.o -lodb-mysql -lodb
  • 请注意,我们将应用程序与两个ODB库链接:libodb是一个公共运行时库,libodb-mysql是一个mysql运行时库(如果您使用另一个数据库,则此库的名称将相应更改)。如果你收到一个错误,说找不到其中一个库,那么你需要使用-L链接器选项来指定它们的位置。
  • 在运行应用程序之前,我们需要使用生成的person.sql文件创建数据库模式。对于MySQL,我们可以使用MySQL客户端程序,例如:
bash 复制代码
mysql --user=odb_test --database=odb_test < person.sql
  • 上述命令将以用户odb_test的身份登录到本地MySQL服务器,无需密码,并使用名为odb_test.的数据库。请注意,执行此命令后,存储在odb_test数据库中的所有数据都将被删除。
  • 还要注意,使用独立生成的SQL文件并不是在ODB中创建数据库模式的唯一方法。我们还可以将模式直接嵌入到我们的应用程序中,或者使用不是由ODB编译器生成的自定义模式。
  • 数据库模式就绪后,我们使用相同的登录名和数据库名称运行应用程序:
bash 复制代码
./driver --user odb_test --database odb_test

4. 使对象持久化

cpp 复制代码
// 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.hxx>和<odb/transaction.hxx>,它们定义了独立于数据库系统的odb::database和odb:;transaction接口。然后我们包括<odb/mysql/database.hxx>,它定义了数据库接口的mysql实现。最后,我们包括person.hxx和person-odb.hxx,它们定义了我们的持久人类。
  • 然后我们有两个使用命名空间指令。第一个引入了标准名称空间中的名称,第二个引入了ODB声明,我们稍后将在文件中使用这些声明。请注意,在第二个指令中,我们使用odb::core命名空间,而不仅仅是odb。前者只将基本的ODB名称(如数据库和事务类)带入当前名称空间,而不带任何辅助对象。这最大限度地减少了与其他库名称冲突的可能性。还要注意,在限定单个名称时,您应该继续使用odb命名空间。例如,您应该编写odb::database,而不是odb::core::database。
  • 一旦我们进入main(),我们做的第一件事就是创建MySQL数据库对象。请注意,这是driver.cxx中明确提到MySQL的最后一行;其余代码通过通用接口工作,并且独立于数据库系统。我们使用argc/argv-mysql::database构造函数,它从命令行自动提取数据库参数,如登录名、密码、数据库名等。在您自己的应用程序中,您可能更喜欢使用其他mysql::数据库构造函数,它们允许您直接传递此信息。
  • 接下来,我们创建三个人对象。现在它们是瞬态对象,这意味着如果我们此时终止应用程序,它们将消失,没有任何存在的证据。下一行启动数据库事务。我们将在本手册稍后详细讨论交易。目前,我们需要知道的是,所有ODB数据库操作都必须在事务中执行,事务是一个原子工作单元;事务中执行的所有数据库操作要么一起成功(提交),要么自动撤消(回滚)。
  • 一旦我们进入事务,我们就对每个person对象调用persist()数据库函数。此时,每个对象的状态都保存在数据库中。但是,请注意,除非提交事务,否则此状态不是永久性的。例如,如果我们的应用程序此时崩溃,仍然没有证据表明我们的对象曾经存在过。
  • 在我们的例子中,当我们调用persist()时,还会发生另一件事。请记住,我们决定为我们的person对象使用数据库分配的标识符。对persist()的调用就是执行此赋值的地方。一旦此函数返回,id_成员将包含此对象的唯一标识符。为了方便起见,persist()函数还返回它持久化的对象标识符的副本。我们将每个对象的返回标识符保存在局部变量中。我们将在稍后使用这些标识符对持久对象执行其他数据库操作。
  • 在我们持久化了对象之后,是时候提交事务并使更改永久化了。只有在commit()函数成功返回后,我们才能保证对象是持久的。继续崩溃示例,如果我们的应用程序在提交后因任何原因终止,数据库中对象的状态将保持不变。事实上,我们很快就会发现,我们的应用程序可以重新启动并从数据库中加载原始对象。还要注意,事务必须通过commit()调用显式提交。如果事务对象在没有显式提交或回滚事务的情况下离开作用域,它将自动回滚。这种行为使您不必担心在事务中抛出异常;如果它们越过事务边界,事务将自动回滚,对数据库所做的所有更改都将撤消。
  • 我们示例中的最后一段代码是处理数据库异常的catch块。我们通过捕获基本ODB异常并打印诊断来实现这一点。
  • 现在让我们编译,然后运行我们的第一个ODB应用程序:
bash 复制代码
mysql --user=odb_test --database=odb_test < person.sql
./driver --user odb_test --database odb_test
  • 我们的第一个应用程序除了错误消息外什么都不打印,所以我们无法真正判断它是否真的将对象的状态存储在数据库中。虽然我们很快就会让我们的应用程序更有趣,但现在我们可以使用mysql客户端来检查数据库内容。它还将让我们了解对象的存储方式:
bash 复制代码
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语句。以下是我们如何在transaction期间启用跟踪:
cpp 复制代码
// 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 ();
    }
  • 经过此修改,我们的应用程序现在产生以下输出:
cpp 复制代码
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使用准备好的语句并以二进制形式将数据发送到数据库。

5. 查询数据库中的对象

到目前为止,我们的应用程序不像典型的"Hello World"示例。除了错误消息外,它不会打印任何内容。让我们改变这一点,并教我们的应用程序向数据库中的人打招呼。为了让它更有趣,让我们只向30岁以上的人打招呼:

cpp 复制代码
// 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为我们的应用程序中经常使用的两个模板实例化创建了方便的别名。第一个是人员对象的查询类型,第二个是该查询的结果类型。
  • 然后我们开始一个新的事务并调用query()数据库函数。我们传递一个查询表达式(query::age>30),它将返回的对象限制为年龄大于30的对象。我们还将查询结果保存在局部变量中。
  • 接下来的几行在结果序列上执行标准的循环迭代,为每个返回的人打印hello。然后我们提交事务,就是这样。让我们看看这个应用程序将打印什么:
bash 复制代码
mysql --user=odb_test --database=odb_test < person.sql
./driver --user odb_test --database odb_test

Hello, John!
Hello, Jane!
  • 这看起来似乎是对的,但我们如何知道查询实际上使用了数据库,而不仅仅是使用了早期persist()调用的一些内存。测试这一点的一种方法是注释掉应用程序中的第一个事务,并在不重新创建数据库模式的情况下重新运行它。这样,将返回上次运行期间持久化的对象。或者,我们可以重新运行同一个应用程序,而不重新创建模式,并注意到我们现在显示了重复的对象:
bash 复制代码
./driver --user odb_test --database odb_test

Hello, John!
Hello, Jane!
Hello, John!
Hello, Jane!
  • 这里发生的事情是,我们应用程序的前一次运行持久化了一组person对象,当我们重新运行应用程序时,我们持久化了另一组同名但ID不同的对象。稍后运行查询时,将返回两个集合中的匹配项。我们可以更改打印"Hello"字符串的行,如下所示,以说明这一点:
cpp 复制代码
cout << "Hello, " << i->first () << " (" << i->id () << ")!" << endl;
  • 如果我们现在重新运行这个修改后的程序,再次不重新创建数据库模式,我们将得到以下输出:
bash 复制代码
./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"对象。

6. 更新持久对象

虽然使对象持久化,然后使用查询选择其中一些是两个有用的操作,但大多数应用程序还需要更改对象的状态,然后使这些更改持久化。让我们通过更新刚刚过生日的Joe的年龄来说明这一点:

cpp 复制代码
// 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;
  }
}
  • 新transaction的开始和结束与前两个相同。一旦进入事务,我们调用load()数据库函数,用Joe的持久状态实例化一个人对象。我们传递Joe的对象标识符,该标识符是我们之前在使此对象持久化时存储的。虽然在这里我们使用std::auto_ptr来管理返回的对象,但我们也可以使用另一个智能指针,例如C++11的std::unique_ptr或TR1、C++11或Boost的shared_ptr。有关对象生命周期管理和可用于此的智能指针的更多信息,请参阅第3.3节"对象和视图指针"。
  • 有了实例化的对象,我们增加了年龄,并调用update()函数来更新数据库中对象的状态。事务提交后,更改将永久生效。
  • 如果我们现在运行这个应用程序,我们将在输出中看到Joe,因为他现在已经30多岁了:
bash 复制代码
mysql --user=odb_test --database=odb_test < person.sql
./driver --user odb_test --database odb_test

Hello, John!
Hello, Jane!
Hello, Joe!
  • 如果我们没有Joe的标识符怎么办?也许这个对象在我们的应用程序的另一次运行中被持久化了,或者被另一个应用程序完全持久化了。假设数据库中只有一个Joe Dirt,我们可以使用查询工具来提出上述事务的另一种实现:
cpp 复制代码
 // 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 ();
    }

7. 定义和使用视图

假设我们需要收集一些关于数据库中存储的人员的基本统计数据。比如总人数,以及最低和最高年龄。一种方法是查询数据库中的所有person对象,然后在迭代查询结果时计算这些信息。虽然这种方法可能适用于只有三个人的数据库,但如果我们有大量的对象,它的效率会非常低。

  • 虽然从面向对象编程的角度来看,它可能不是概念上纯粹的,但关系数据库可以比我们自己在应用程序的过程中执行相同的操作更快、更经济地执行一些计算。
  • 为了支持这种情况,网上解决机构提供了视图的概念。ODB视图是一个C++类,它体现了一个或多个持久对象或数据库表的轻量级只读投影,或者是本机SQL查询执行或存储过程调用的结果。
  • 视图的一些常见应用包括从对象或列数据库表加载数据成员的子集,执行和处理任意SQL查询的结果,包括聚合查询,以及使用对象关系或自定义连接条件连接多个对象和/或数据库表。
  • 以下是我们如何定义person_stat视图,该视图返回有关person对象的基本统计信息:
cpp 复制代码
#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;
};
  • 通常,为了获得视图的结果,我们使用与在数据库中查询对象时相同的query()函数。然而,在这里,我们执行的是一个聚合查询,它总是只返回一个元素。因此,我们可以使用快捷方式query_value()函数,而不是获取结果实例并对其进行迭代。以下是我们如何使用刚刚创建的视图加载和打印统计数据:
cpp 复制代码
    // 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_stat视图添加到person.hxx标头中,将上述事务添加到driver.cxx,并重新编译和运行我们的示例,那么我们将在输出中看到以下附加行:
cpp 复制代码
count  : 3
min age: 31
max age: 33

8. 删除持久化对象

我们将讨论的最后一个操作是从数据库中删除持久对象。以下代码片段显示了如何删除给定标识符的对象:

cpp 复制代码
  // John Doe is no longer in our database.
    //
    {
      transaction t (db->begin ());
      db->erase<person> (john_id);
      t.commit ();
    }
  • 为了从数据库中删除John,我们启动一个事务,使用John的对象id调用erase()数据库函数,并提交事务。事务提交后,被擦除的对象不再持久。
  • 如果我们手头没有对象id,我们可以使用查询来查找和删除对象:
cpp 复制代码
	// 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 ();
    }

9. 更改持久类

当临时C++类的定义发生变化时,例如通过添加或删除数据成员,我们不必担心该类的任何现有实例与新定义不匹配。毕竟,为了使类更改有效,我们必须重新启动应用程序,并且没有一个瞬态实例能够幸存下来。

  • 对于持久类来说,事情并不那么简单。因为它们存储在数据库中,因此在应用程序重启后仍然存在,所以我们遇到了一个新问题:一旦我们更改了持久类,现有对象(对应于旧定义)的状态会发生什么变化?
  • 使用旧对象的问题称为数据库模式演化,这是一个复杂的问题,ODB为处理它提供了全面的支持。
  • 假设在使用我们的person持久类一段时间并创建了许多包含其实例的数据库后,我们意识到对于某些人来说,我们还需要存储他们的中间名。如果我们继续添加新的数据成员,那么新数据库的一切都会正常工作。然而,现有数据库的表与新的类定义不对应。具体来说,生成的数据库支持代码现在期望有一个列来存储中间名。但在旧数据库中从未创建过这样的列。
  • ODB可以自动生成SQL语句,这些语句将迁移旧数据库以匹配新的类定义。但首先,我们需要通过为我们的对象模型定义一个版本来启用模式演化支持:
cpp 复制代码
// person.hxx
//

#pragma db model version(1, 1)

class person
{
  ...

  std::string first_;
  std::string last_;
  unsigned short age_;
};
  • 版本pragma中的第一个数字是基本模型版本。这是我们能够迁移的最低版本。第二个数字是当前型号版本。由于我们还没有对持久类进行任何更改,因此这两个值都是1。
  • 接下来,我们需要使用ODB编译器重新编译person.hxx头文件,就像之前一样:
bash 复制代码
odb -d mysql --generate-query --generate-schema person.hxx

-如果我们现在查看ODB编译器生成的文件列表,我们会注意到一个新文件:person.xml。这是一个更改日志文件,ODB编译器在其中跟踪与我们的类更改相对应的数据库更改。请注意,此文件由ODB编译器自动维护,我们所要做的就是在重新编译之间保留它。

  • 现在,我们已经准备好将中间名添加到person类中。我们还为它提供了一个默认值(空字符串),该值将分配给旧数据库中的现有对象。请注意,我们还增加了当前版本:
cpp 复制代码
// 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.hxx标头,我们将看到两个额外生成的文件:person-002-pre.sqlperson-002-post.sql。这两个文件包含从版本1到版本2的模式迁移语句。与模式创建类似,模式迁移语句也可以嵌入到生成的C++代码中。
  • person-002-pre.sqlperson-002-post.sql是模式迁移前后的文件。要迁移我们的一个旧数据库,我们首先执行预迁移文件:
bash 复制代码
mysql --user=odb_test --database=odb_test < person-002-pre.sql
  • 如果需要,我们可以在模式迁移前后运行数据迁移代码。在这个阶段,我们既可以访问旧数据,也可以存储新数据。在我们的例子中,我们不需要任何数据迁移代码,因为我们为所有现有对象的中间名分配了默认值。
  • 为了完成迁移过程,我们执行迁移后语句:
bash 复制代码
mysql --user=odb_test --database=odb_test < person-002-post.sql

10. 使用多个数据库

访问多个数据库(即数据存储)只需创建多个代表每个数据库的odb::<db>:数据库实例即可。例如:

cpp 复制代码
odb::mysql::database db1 ("john", "secret", "test_db1");
odb::mysql::database db2 ("john", "secret", "test_db2");
  • 一些数据库系统还允许将多个数据库附加到同一实例。一个更有趣的问题是,我们如何从同一应用程序访问多个数据库系统(即数据库实现)。例如,我们的应用程序可能需要将一些对象存储在远程MySQL数据库中,而将其他对象存储在本地SQLite文件中。或者,我们的应用程序可能需要能够将其对象存储在用户在运行时选择的数据库系统中。
  • ODB提供全面的多数据库支持,从与特定数据库系统的紧密集成到能够编写与数据库无关的代码以及动态加载单个数据库系统支持。
  • 添加多数据库支持的第一步是重新编译person.hxx标头,为其他数据库系统生成数据库支持代码:
bash 复制代码
odb --multi-database dynamic -d common -d mysql -d pgsql \
--generate-query --generate-schema person.hxx
  • --multi-database ODB编译器选项启用多数据库支持。目前,我们传递给此选项的动态值的含义并不重要,但如果你好奇,请参阅第16章。此命令的结果是生成三组文件:person-odb。?xx(公共接口;对应公共数据库),person-odb-mysql。?xx(MySQL支持代码)和person-odb-pgsql。?xx(PostgreSQL支持代码)。还有两个模式文件:person-mysql.sql和person-pgsql.sql。
  • 我们需要在driver.cxx中更改的唯一部分是如何创建数据库实例。具体来说,这条线:
cpp 复制代码
auto_ptr<database> db (new odb::mysql::database (argc, argv));
  • 现在,我们的示例能够将数据存储在MySQL或PostgreSQL中,因此我们需要以某种方式允许调用者指定我们必须使用哪个数据库。为了简单起见,我们将使第一个命令行参数指定我们必须使用的数据库系统,而其余参数将包含特定于数据库的选项,我们将像以前一样将这些选项传递给odb:::database构造函数。让我们把所有这些逻辑放在一个单独的函数中,我们将调用create_database()。以下是我们修改后的driver.cxx的开头(其余部分不变):
cpp 复制代码
// 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.

    ...
  • 就是这样。剩下的就是构建和运行我们的示例:
bash 复制代码
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数据库:
bash 复制代码
mysql --user=odb_test --database=odb_test < person-mysql.sql
./driver mysql --user odb_test --database odb_test
  • 或者PostgreSQL数据库:
bash 复制代码
psql --user=odb_test --dbname=odb_test -f person-pgsql.sql
./driver pgsql --user odb_test --database odb_test

11. 总结

本章介绍了一个非常简单的应用程序,尽管如此,它还是执行了所有核心数据库功能:persist()、query()、load()、update()和erase()。我们还看到,编写使用ODB的应用程序涉及以下步骤:

  1. 在头文件中声明持久类。
  2. 编译这些标头以生成数据库支持代码。
  3. 将应用程序与生成的代码和两个ODB运行时库链接起来。

如果在这一点上,很多事情似乎都不清楚,不要担心。本章的目的只是让您大致了解如何使用ODB持久化C++对象。我们将在本手册的其余部分介绍所有细节。

相关推荐
夏木~10 分钟前
Oracle 中什么情况下 可以使用 EXISTS 替代 IN 提高查询效率
数据库·oracle
W215512 分钟前
Liunx下MySQL:表的约束
数据库·mysql
黄名富17 分钟前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
言、雲22 分钟前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
OopspoO26 分钟前
qcow2镜像大小压缩
学习·性能优化
东风吹柳41 分钟前
观察者模式(sigslot in C++)
c++·观察者模式·信号槽·sigslot
A懿轩A1 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
居居飒1 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
一个程序员_zhangzhen1 小时前
sqlserver新建用户并分配对视图的只读权限
数据库·sqlserver
zfj3211 小时前
学技术学英文:代码中的锁:悲观锁和乐观锁
数据库·乐观锁··悲观锁·竞态条件