C++ Hibernate






4.74/5 (9投票s)
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
”添加到预处理器定义中。
在“代码生成”页面上,我们为 SQLComponents
和 Marlin
类型的异常启用了异步异常。
最后一步是添加指向“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 ==========
然而,如果我们不指定 Marlin
和 SQLComponents
框架所需的其他强制性 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
方法从简单的单参数加载到带有复杂过滤器集返回对象集的加载。这些是 CXSession
的 load
方法:
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
”的派生类,该对象存储在两个表中(cat
和 animal
)。
这是一个两阶段插入的示例:
...
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>
”运算符。这将在 CXSession
的 Delete(CXObject*)
方法中完成!
默认实现
当您遵循默认实现时,例如,通过 CFG2CPP 工具生成类实现,您将获得:
- 一个头文件,其中包含所有数据成员和所有数据成员的默认 setter 和 getter。类声明包含一个(1)额外的宏用于声明序列化工厂(“
DECLARE_CXO_SERIALIZATION
”)。
- 一个工厂实现文件,包含对象对于数据库和 SOAP 消息的序列化和反序列化。它还包含 Hibernate 框架用于创建此类新对象的 create-object 工厂。
大多数情况下,您可以将此实现文件单独保留。只有当您向类添加新属性(以及数据库列)时,才需要在此处进行修改并添加这些属性。您可以执行以下任一操作:
- 使用“
cfg2cpp
”工具创建一个新文件集,在更改了 config 文件后,提取出 *.cxh.cpp 文件; - 通过向文件中添加行来添加属性。已采取极大的谨慎使这四种实现保持一致,以便于为新属性添加行。
一个仅包含构造函数和析构函数的实现文件。您现在可以立即在此文件中开始实现您的类。没有可见的开销挡道。所以这确实是 C++ 的方式:零开销。
更多
完整文档(参见文档目录)包含更多关于以下内容的信息:
- 持久化类及其可能性。
- O/R 映射方法。
- 支持类(和表)之间的数据库关联。
- 过滤器以及如何在加载时表达对象搜索。
- 事务和批量处理。
- 拦截事件,如“
OnLoad
”和“OnSave
”等。 - 数据类型和 SQL 变体(以及关于它们的乐趣)。
- 更多关于生成 *.cpp、*.h 和 *_chx.cpp 文件的工具的信息。
- 附录关于您可以在此库中使用的数据类型、运算符和类型转换。
事实上,完整手册比本文长得多。
玩得开心
因此:在 C++ 中玩转 Hibernate 吧。
历史
- 2019年9月25日:初始版本