65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ Hibernate

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (9投票s)

2019年9月25日

MIT

14分钟阅读

viewsIcon

12632

downloadIcon

388

C++ 的 Hibernate 框架。

什么?

CXHibernate 是一个用于与持久化对象存储进行通信的数据库框架。最常见的是数据库,但文件存储或互联网上的普通存储也是持久化对象的可能性。由于 CXHibernate 是一个 C++ 框架,它使用 C++ 对象。这些对象可以存储、检索、更新或删除 SQL 数据库,这些数据库通过通用的 ODBC 标准进行接口。所有 SQL 数据库都拥有由 Microsoft ODBC 标准定义的通用 Open Database Connectivity 层。

Java 和 C# .NET 等其他语言多年来一直拥有 Hibernate 框架,以减轻程序员在处理数据库方面的痛苦。您可以在互联网上找到这些框架的文档(Java Hibernate:https://hibernate.com.cn/orm/documentation 和 C# .NET NHibernate https://nhibernate.info/doc/)。

为什么?

使用数据库可能是一项困难且耗时的工作。不仅存在映射面向对象类的任务,还存在将对象在数据库中进行 `SELECT`、`INSERT`、`UPDATE` 和 `DELETE` 的低级操作编程的所有细节。

Hibernate 是一种范例,它极大地简化了从程序员角度处理数据库的任务。虽然它不能让他或她免于处理数据库的细节,但使用持久化对象的标准工作流程却很简单。它充当您的应用程序和数据库及其驱动程序之间的中间层。CXHibernate 支持多种数据库平台、数据类型和对象关系映射。因此,它是一个 ORM (= 对象关系映射器)。

架构

CXHibernate 架构的核心工作对象是“session”。session 是您的工作单元,它使您能够访问对象缓存、数据库、文件存储以及(通过互联网)其他网络位置的数据存储。

持久化的对象可以直接从应用程序中处理,就像它们是“常规”对象一样。它们可以通过 session “找到”。Session 将首先尝试在缓存中查找对象,然后在第二次尝试在不同的存储“位置”查找。

未被跟踪的对象被引用为“瞬态”对象。这意味着当程序关闭时它们将“消失”,并且不会持久化到数据库、互联网或文件存储层。

在这三种情况下,处理对象的方式没有区别。

config.xml 文件(默认是‘hibernate.cfg.xml’)描述了您应用程序中 **以及** 存储层中的数据类。

您可以将两个应用程序链接在一起形成一个“云存储”。客户端将从位于“云中某处”的服务器端请求对象。除了应用程序的配置外,存储和检索对象与从数据库存储和检索对象没有区别。此云存储配置在下图所示

您的应用程序的标准配置通常包含在应用程序根目录的“hibernate.cfg.xml”文件中。这是一个通用的 XML 文件,其中包含您应用程序中所有类的定义、它们的属性以及它们的关联。当您使用默认名称时,加载此文件是透明的。

CXSession* session = hibernate.GetSession();

当请求新的工作 session 时,可以使用通用接口加载任何其他名称。这可以通过请求显式 session 从备用配置来实现,如下所示:

CXSession* session = hibernate.LoadConfiguration(“ses”, “C:\Path-to-app\My_config.xml”);

这足以让您开始。或者,您也可以在调用此调用时指定一个不同的文件作为参数。此文件的 *.cxh 扩展名仅为约定,而非必需。XML 配置文件包含应用程序和 session 的通用参数,以及所有类的定义。看起来是这样的:

<hibernate>
  <strategy>standalone</strategy>
  <logfile>C:\TMP\My_hibernate_logfile.txt</logfile>
  <loglevel>6</loglevel>
  <database_use>use</database_use>
  <class>
    <name>country</name>
    <schema>data</schema>
    <table>country</table>
    <discriminator>cty</discriminator>
    <attributes>
      <attribute name="id" datatype="int" generator="true"isprimary="true" />
      <attribute name="name"  datatype="string" maxlength="100" />
      <attribute name="inhabitants" datatype="int" />
      <attribute name="continent" datatype="string" maxlength="20" />
    </attributes>
    <identity name="pk_country">
      <attribute name="id" />
    </identity>
    <generator name="country_seq" start="1" />
  </class>
</hibernate>

事实上:这就是下一段示例所需的一切。

正如您所见:配置文件包含一些通用设置,然后包含一个或多个类及其结构。无论是没有面向对象层次结构的独立类,还是复杂的层次结构、类关联、索引等。

最基本的事实是,类描述命名了您类中(因此也命名了数据库表中)的所有瞬态属性。当然,您的应用程序的对象可以拥有比这些属性更多的数据成员,但这些是将在数据库中持久化的成员。

主键列(在此例中为“id”)需要特别注意。对象的新实例由生成器创建(从数字‘1’开始)。在数据库中,此列将是主键的一部分,从而形成对象的标识以及数据库表中记录的标识。

业务键——因此也是主键——可以由多个列组成,但其中一列被分配给具有‘generator=”true”’属性的序列生成器。

一个基本的“Hello World”示例

按照介绍程序员熟悉新范例的悠久传统,我们将使用 CXHibernate 编写数据库版本的“Hello World!”。

此演练从 Visual Studio 2017 中的新解决方案目录“HelloWorld”和解决方案文件开始(任何版本的 Visual Studio 都可以)。我们从标准的“Windows 控制台应用程序”开始。

确保“取消选择”‘创建新的 Git 存储库’选项。

从 github https://github.com/edwig/cxhibernate,我们添加以下组件目录:

  • CXHibernate
  • SQLComponents
  • Marlin

添加这些组件后,解决方案目录应如下所示:

(抱歉,这是荷兰语的资源管理器,是的,‘GROOT’不是行走的树,它确实意味着‘BIG’。)

复制完这三个组件目录后,我们可以将这些组件的项目文件包含到我们的解决方案中。只需在“Hello World”解决方案的解决方案级别使用“添加…”和“现有项目…”选项。

包含三个项目文件后,您的解决方案应如下所示:

现在,在我们开始在“HelloWorld.cpp”文件中编程之前,我们需要更改一些项目设置,以便能够使用这三个添加的组件。我们需要做的第一个设置是将“字符集”更改为“使用多字节字符集”。

Hibernate 模块都编译为静态链接库。这样做是为了避免在安装应用程序时出现“DLL Hell”问题。但是,如果您愿意,当然可以随时更改它。

其次,整个框架在西欧构建,没有对 Unicode 和进一步国际化的需求或强调。所以目前一切只在 MBCS 字符集下工作。

Unicode UTF-8 或 UTF-16 版本在愿望清单上。

我们继续处理项目所需的包含路径。编译器需要找到额外的组件及其头文件,因此我们添加以下路径:

  • $(SolutionDir)CXHibernate\
  • $(SolutionDir)SQLComponents\
  • $(SolutionDir)Marlin\

这是通过 C++ 常规属性页,在第一行“其他包含目录”中完成的。

在“预处理器”页面上,我们将“COMPILED_WITH_MARLIN”添加到预处理器定义中。

在“代码生成”页面上,我们为 SQLComponentsMarlin 类型的异常启用了异步异常。

最后一步是添加指向“Lib”目录的路径,以便与“Marlin”、“SQLComponents”和“CXHibernate”模块的结果库链接。转到“链接器 / 常规”页面,并在“其他库目录”设置中填入“$(SolutionDir)Lib\”路径。

好的。我们准备好了。您现在基本上可以编译应用程序了,但在此之前,我们需要向“main()”函数添加代码,并向我们的应用程序添加一个名为“Country”的持久化类。

要快速创建一个持久化类,请将第 2 章的配置文件添加到我们的“Hello World”目录中,并在命令行中使用以下选项运行“CXH2CPP”实用程序:

CXH2CPP Country

这将生成“country.h”、“country.cpp”和“country_cxh.cpp”文件。将这些文件包含到您的“Hello World 项目”中。

在编译它们之前,您需要对“stdafx.h”文件进行最后一次修改。您必须将以下内容添加到 **文件末尾**:

#include <afx.h>
#include <SQLComponents.h>
#include <CXHibernate.h>
#include <Marlin.h>

这不仅允许您使用 MFC,还可以使用 CXHibernate。此外,特定于您的配置和平台的库名称将自动在 Visual Studio 中配置。

现在编译单个文件或多个文件将自动链接到库。

1>------ Build started: Project: HelloWorld, Configuration: Debug x64 ------
1>stdafx.cpp
1>Automatically linking with SQLComponents_x64D.lib
1>Automatically linking with CXHibernate_x64D.lib
1>Automatically linking with Marlin_x64D.lib
1>country.cpp
1>country_cxh.cpp
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========

然而,如果我们不指定 MarlinSQLComponents 框架所需的其他强制性 MS-Windows 组件集,我们就无法构建所需的运行时。否则,系统链接器将报错“未解析的外部符号”。

附加组件是:

  • odbc32.lib:用于 ODBC 和 ODBC-Manager 函数。
  • Rpcrt4.lib:用于生成 Microsoft GUID。
  • httpapi.lib:用于服务器访问 HTTP 服务协议。
  • winhttp.lib:用于客户端访问 HTTP 协议。
  • crypt32.lib:用于加密的 Web 服务。
  • secur32.lib:用于

将它们添加到项目文件的“链接器 / 输入”页面。

好的,现在我们拥有了所有东西。我们的项目应该看起来像:

一切都应该可以正常编译,只是它还没有做任何事情(还没有)。

但是,让我们先快速看一下为我们的“country”类生成的文件。

这是 *.CPP 文件的实现:

// Implementation file for class: Country
// Automatically generated by: CX-Hibernate
//
#include "stdafx.h"
#include "Country.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

// CTOR for class

Country::Country()
{
  // Things to do in the constructor
}

// DTOR for class

Country::~Country()
{
  // Things to do in the destructor
}

以及接口文件(*.H):

// Interface definition file for class: Country
// Automatically generated by: CX-Hibernate
// File: country.h
//
#pragma once

#include <CXObject.h>
#include <bcd.h>
#include <SQLDate.h>
#include <SQLTime.h>
#include <SQLTimestamp.h>
#include <SQLInterval.h>
#include <SQLGuid.h>
#include <SQLVariant.h>

class Country : public CXObject
{
public:
  // CTOR of an CXObject derived class
  Country();
  // DTOR of an CXObject derived class
  virtual ~Country();

  // Serialization of our persistent objects
  DECLARE_CXO_SERIALIZATION;

  // GETTERS
  int        GetId()              { return m_id;                };
  CString    GetName()            { return m_name;              };
  int        GetInhabitants()     { return m_inhabitants;       };
  CString    GetContinent()       { return m_continent;         };

protected:
  // Database persistent attributes
  int        m_id                 { 0 };
  CString    m_name              ;
  int        m_inhabitants        { 0 };
  CString    m_continent         ;

private:
  // Transient attributes go here
};

现在我们生成了“country_cxh.cpp”文件。这是我们进行序列化和反序列化的地方。这取代了 Hibernate 的其他变体可以进行反射的地方。C++ 没有元数据,因此序列化由这些宏完成。

// (De-)Serializing factories for class: Country
// Generated by CX-Hibernate cfg2cpp tool
//
#include "stdafx.h"
#include "Country.h"
#include <SQLRecord.h>
#include <SOAPMessage.h>

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

BEGIN_XML_SERIALIZE(Country,CXObject)
  CXO_XML_SERIALIZE(int     ,m_id          ,"id"          ,XDT_Integer);
  CXO_XML_SERIALIZE(CString ,m_name        ,"name"        ,XDT_String);
  CXO_XML_SERIALIZE(int     ,m_inhabitants ,"inhabitants" ,XDT_Integer);
  CXO_XML_SERIALIZE(CString ,m_continent   ,"continent"   ,XDT_String);
END_XML_SERIALIZE

BEGIN_XML_DESERIALIZE(Country,CXObject)
  CXO_XML_DESERIALIZE(int     ,m_id          ,"id"         ,XDT_Integer);
  CXO_XML_DESERIALIZE(CString ,m_name        ,"name"       ,XDT_String);
  CXO_XML_DESERIALIZE(int     ,m_inhabitants ,"inhabitants",XDT_Integer);
  CXO_XML_DESERIALIZE(CString ,m_continent   ,"continent"  ,XDT_String);
END_XML_DESERIALIZE

BEGIN_DBS_SERIALIZE(Country,CXObject)
  CXO_DBS_SERIALIZE(int       ,m_id          ,"id"         ,XDT_Integer);
  CXO_DBS_SERIALIZE(CString   ,m_name        ,"name"       ,XDT_String);
  CXO_DBS_SERIALIZE(int       ,m_inhabitants ,"inhabitants",XDT_Integer);
  CXO_DBS_SERIALIZE(CString   ,m_continent   ,"continent"  ,XDT_String);
END_DBS_SERIALIZE

BEGIN_DBS_DESERIALIZE(Country,CXObject)
  CXO_DBS_DESERIALIZE(int     ,m_id          ,"id"         ,XDT_Integer);
  CXO_DBS_DESERIALIZE(CString ,m_name        ,"name"       ,XDT_String);
  CXO_DBS_DESERIALIZE(int     ,m_inhabitants ,"inhabitants",XDT_Integer);
  CXO_DBS_DESERIALIZE(CString ,m_continent   ,"continent"  ,XDT_String);
END_DBS_DESERIALIZE

BEGIN_DESERIALIZE_GENERATOR(Country)
  CXO_DBS_DESERIALIZE(long    ,m_id          ,"id"         ,XDT_Integer);
END_DESERIALIZE_GENERATOR

// Static factory to create a new object if this class
DEFINE_CXO_FACTORY(Country);

现在终于可以开始工作了。我们可以开始填充应用程序的“main()”函数了。从全局“hibernate”对象请求一个 session,并将第一个 id=1 的国家加载到内存中。如果一切顺利,我们可以直接开始调用对象的方法,并在控制台上打印“Hello world”。

完成工作后,您可以选择关闭 session。

当然,您还应该有一个名为“hibtest”的 ODBC 连接到 CX-Hibernate 项目的测试数据库。在这种情况下,是 Firebird 3.0 数据库,其中包含“COUNTRY”表。

(填充自:https://simple.wikipedia.org/wiki/List_of_countries_by_population

// HelloWorld.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include "Country.h"
#include <CXHibernate.h>
#include <CXSession.h>

int main()
{
  CXSession * session = hibernate.CreateSession();

  if(session)
  {
    // Set a database session
    session->SetDatabaseConnection("hibtest","sysdba","altijd");

    Country* land = (Country*)session->Load(Country::ClassName(),1);
    if(land)
    {
      printf("Hallo World! to all %d inhabitants of %s\n"
            ,land->GetInhabitants()
            ,land->GetName().GetString());
    }
    else
    {
      printf("Cannot find a country with id = %d\n",1);
    }
    // And close our session
    session->CloseSession();
  }
  return 0;
}

现在编译,将“hibernate.cfg.xml”复制到运行时目录并运行它!

瞧!

这结束了我们的“Hello World”第一个示例。当然,您现在可以继续为这个简单的第一个程序创建各种额外的测试。建议您尝试:

  • 向程序添加一个额外的参数来请求不同的国家。
  • 添加一个带有过滤器的加载,以请求一组国家并打印所有国家。
  • 计算世界上所有的居民,并向他们所有人说“Hello”,传播“和平与幸福”给世界 :-)。

基本操作

在上一章通过“Hello World”示例向您介绍了 CX-Hibernate 后,现在让我们看看基本的 Hibernate 操作及其衍生操作。基本操作是:

  • Load:从外部存储(数据库、Web 服务、文件存储)获取对象。
  • Insert:将新对象“冬眠”到外部存储中。
  • Update:更改“冬眠”存储中的对象,使其反映应用程序中的对象。
  • Save:插入新对象或更新现有加载的对象。
  • Delete:从“冬眠”存储中删除对象,很可能也从您的应用程序中删除。

基本操作是通用 CXSession 类的成员函数。每个应用程序用户都必须有一个活动 session,才能对外部存储执行基本操作。

对于每个基本操作,下面几段将描述其变体和要求。

加载对象

Session 中加载对象有各种重载。这是为了方便程序员找到最简单的对象加载途径。load 方法从简单的单参数加载到带有复杂过滤器集返回对象集的加载。这些是 CXSessionload 方法:

CXObject* Load(CString p_className,int p_primary);
CXObject* Load(CString p_className,CString p_primary);
CXObject* Load(CString p_className,SQLVariant* p_primary);
CXObject* Load(CString p_className,VariantSet& p_primary);
CXResultSet Load(CString p_className,SQLFilter* p_filter);
CXResultSet Load(CString p_className,SQLFilterSet& p_filters);

有如此多的“Load”操作,是为了适应数据库对象主键由多个数据库列组成的各种情况。在许多只有单个“id”列的情况下,第一个带有“integer”主键的“Load”操作就足够了。在复合主键的情况下,带有“VariantSet”和“SQLFilterSet”的变体是可行的方法。实际上,这些变体在内部被其他“Load”操作调用。

第二件要注意的事情是,所有“Load”操作的第一个参数都是类的名称。此参数可以填写为 static string,例如“classname”(不区分大小写!),但使用类的对象工厂中的“ClassName”会更简单、更自然。我们在上一章的 HelloWorld 程序中已经看到了这样的示例,如下所示:

...
Country* land = (Country*)session->Load(Country::ClassName(),1);
...

插入对象

这是一个两步过程。插入对象首先需要创建一个特定持久化类的对象。在填充了对象数据,也许还有一个部分主键后,我们可以要求 Hibernate 将对象插入到外部存储中。

由于“Cat”是“Animal”的派生类,该对象存储在两个表中(catanimal)。

这是一个两阶段插入的示例:

...
CXSession* session = hibernate.CreateSession();
...

Cat* pussy = (Cat*) session->CreateObject(Cat::ClassName());

// Fill in our object with reasonable values
pussy->SetAnimalName("Silvester");
pussy->SetHas_claws(true);
pussy->SetLikesBirds("Tweety");

// Go save it in the database in two tables (Animal and Cat)
bool result = session->Insert(pussy);
...

除了“Insert”之外,您还可以使用“Save”作为等效操作。

调用 session 的“CreateObject”方法而不是“new”对象。这确保了对象是由对象工厂创建的,并且 Hibernate 框架所需的所有必要操作都已完成。 **切勿“new”您的对象!**

将对象插入外部存储的一个副作用是,基类 CXObject 中的主键将被填充,这反过来会改变对象的“瞬态”状态为“持久化”。

另一个副作用是对象被引用在对象缓存中。您不必在应用程序中跟踪它以释放。Hibernate 框架会为您处理。

更新对象

更新单个对象非常简单。只需调用 session 的“Update”方法即可。就是这样!

这是一个例子。

...
CXSession* session = hibernate.CreateSession();
...
Cat* pussy = (Cat*) session->Load(Cat::ClassName(),42);

// change the state of the object
pussy->SetColor("black-and-white");

// Go update the external store
bool result = session->Update(pussy);
...

删除对象

删除单个对象非常简单。只需调用 session 的“Delete”方法即可。就是这样!

这是一个例子。

...
CXSession* session = hibernate.CreateSession();
...
// Load a certain object
Cat* pussy = (Cat*) session->Load(Cat::ClassName(),13);

// Go delete from the external store
bool result = session->Delete(pussy);
...

**请记住**:如果删除成功(返回值为‘true’),则对象将从 Hibernate 缓存中移除 **并且** 会被 C++ 语言的“delete”操作销毁。您的对象指针将不再有效!

只有当“Delete”操作返回‘false’时(无论对象是否从数据库或缓存中删除),指针仍然有效。在可选地记录此事实后,调用者(您!)必须对对象调用“Delete”。

请注意,**无需**对指针使用 C++“delete <pointer>”运算符。这将在 CXSessionDelete(CXObject*) 方法中完成!

默认实现

当您遵循默认实现时,例如,通过 CFG2CPP 工具生成类实现,您将获得:

  • 一个头文件,其中包含所有数据成员和所有数据成员的默认 setter 和 getter。类声明包含一个(1)额外的宏用于声明序列化工厂(“DECLARE_CXO_SERIALIZATION”)。
  • 一个工厂实现文件,包含对象对于数据库和 SOAP 消息的序列化和反序列化。它还包含 Hibernate 框架用于创建此类新对象的 create-object 工厂。

大多数情况下,您可以将此实现文件单独保留。只有当您向类添加新属性(以及数据库列)时,才需要在此处进行修改并添加这些属性。您可以执行以下任一操作:

  1. 使用“cfg2cpp”工具创建一个新文件集,在更改了 config 文件后,提取出 *.cxh.cpp 文件;
  2. 通过向文件中添加行来添加属性。已采取极大的谨慎使这四种实现保持一致,以便于为新属性添加行。

一个仅包含构造函数和析构函数的实现文件。您现在可以立即在此文件中开始实现您的类。没有可见的开销挡道。所以这确实是 C++ 的方式:零开销。

更多

完整文档(参见文档目录)包含更多关于以下内容的信息:

  • 持久化类及其可能性。
  • O/R 映射方法。
  • 支持类(和表)之间的数据库关联。
  • 过滤器以及如何在加载时表达对象搜索。
  • 事务和批量处理。
  • 拦截事件,如“OnLoad”和“OnSave”等。
  • 数据类型和 SQL 变体(以及关于它们的乐趣)。
  • 更多关于生成 *.cpp*.h*_chx.cpp 文件的工具的信息。
  • 附录关于您可以在此库中使用的数据类型、运算符和类型转换。

事实上,完整手册比本文长得多。

玩得开心

因此:在 C++ 中玩转 Hibernate 吧。

历史

  • 2019年9月25日:初始版本
© . All rights reserved.