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

YB.ORM 简介 - C++ 对象关系映射器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (8投票s)

2014年10月30日

MIT

7分钟阅读

viewsIcon

22390

downloadIcon

378

C++ 库 YB.ORM 如何通过领域类来操作存储在 RDBMS 中的数据。ORM 的基本概念通过示例进行解释。

YB.ORM for C++ 简介

YB.ORM logo

本文可能首先对那些用 C++ 开发数据库应用程序的人来说很有趣。我们也会讨论一些基础知识,但仍然推荐具备数据库和 C++ 中级水平。

YB.ORM 旨在简化与关系数据库交互的 C++ 应用程序的开发。对象关系映射器 (ORM) 通过将数据库表映射到类,将表行映射到应用程序中的对象来工作。这种方法可能并非对每个数据库应用程序都最优,但事实证明,在需要复杂逻辑和事务处理的应用程序中是合理的。虽然它还在开发中,但大部分功能已经可以探索。YB.ORM 项目的目标是

  • 为 C++ 开发者提供一个方便的 API
  • 保持 C++ 的高性能
  • 保持源代码易于跨不同平台和编译器移植
  • 支持大多数主流关系数据库管理系统 (DBMS)

该工具采用了 Martin Fowler 的书籍《企业应用架构模式》中的许多概念,例如“懒加载”、“身份映射”、“工作单元”等。此外,该项目的发展受到了 Java 的 Hibernate 框架以及特别是 Python 的 SQLAlchemy 的强大功能的启发。

关于对象关系映射

关系数据库现在非常普遍——从 Oracle 集群到嵌入式 SQLite 文件型数据库。关系数据库以具有列和行的矩形表形式操作数据。它们之所以如此受欢迎,有以下原因:

  • 它们有一个简单的底层数学模型——所谓的数据库代数
  • 有一个标准且功能强大的 SQL 语言可以与数据库交互,嗯,大部分是标准的
  • 有大量的供应商和产品可以满足各种数据存储需求

但是,从应用程序代码接口连接 SQL 数据库并非易事。看看普通的 ODBC API 吧。针对数据库运行 SQL 语句的典型步骤可能包括以下几点:

  1. 连接到数据库,提供参数:主机、端口、架构、用户名、密码等。
  2. 使用连接句柄,创建游标准备 SQL 语句,提供 SQL 语句作为文本。
  3. 使用游标句柄,可选地将输出参数绑定到输出变量,这些变量将在提取完成后接收其值。
  4. 使用游标句柄,可选地绑定输入参数,无论是位置参数还是命名参数,到输入变量。
  5. 可选地为输入变量赋值。
  6. 使用游标,执行准备好的语句,可选地继续步骤 5。
  7. 可选地提取结果集的下一行,如果成功则查看输出变量,可选地重复步骤 7。
  8. 关闭游标。
  9. 关闭连接

SQL 数据库的数据类型与 C 或 C++ 中的数据类型不完全匹配。更不用说数据库中存储的值可能未定义,并且并非每种语言都支持未定义值。

所以应该有一种方法来自动化这些步骤。像 C++ 的 SOCI 这样的库在将 SQL 语句发送到数据库和检索结果方面做得很好。但是,在应用程序中硬编码 SQL 语句还存在另一系列问题。想象一下,你在代码的几个位置从同一个表中使用不同的过滤器运行 SELECT。某一天,你不得不添加或重命名该表中的某个列…

对象关系映射 (ORM) 工具有助于创建额外的抽象层,该层旨在简化 RDBMS 中的数据操作。这个层也称为领域模型。也就是说,一个启用 ORM 的应用程序通常包含很少甚至没有内联 SQL 代码。相反,所有插入、更新、删除和数据检索都通过映射到表和行的领域类和对象进行。基本上,具有成员的领域类对象对应于映射表中具有列的单个行。ORM 层负责发出实际的 SQL 语句来反映对域对象所做的所有更改。

在一定程度上,ORM 将 SQL 语法隐藏在面向对象的 fachada 后面。但如果期望这项技术能够“拯救”开发人员免于学习 SQL 和数据库设计,那就太天真了。它节省了开发人员花费在编写和调试数据库交互样板代码上的时间。尽管如此,拥有一款 ORM 库来处理所有这些 SQL 方言特性,有助于创建可移植的应用程序,这仍然很方便。作为奖励,这种方法还可以保护您的应用程序免受 SQL 代码注入的侵害。此外,在 C++ 这样的静态类型语言中,ORM 方法在编译时强制执行参数类型检查,这也是一件好事。

库的示例用法

使用 ORM 从模型定义开始。它可以是包含表和关系的 XML 文件,或者是类声明中的内联宏,或者是一个处理访问者的模板函数。此步骤可能需要也可能不需要代码生成或其他预处理。这些变体中的每一种都有其优点和缺点。

让我们考虑一个包含两个实体:ClientOrder 的示例模式。它们之间存在一对多关系:一个 Client 可以有零个或多个 Order,每个 Order 属于一个 ClientClients 存储在 client_tbl 表中,而它们的 Order 存储在 order_tbl 表中。

在 SQL 层面,这种关系可以表示为外键约束,它位于子表 order_tblclient_id 列上,引用父表 client_tbl主键 id 列。从 ORM 的角度来看,这种关系通常由对象的属性表示。Order 类的实例有一个对象引用属性,它引用 Client 类的一个父对象。从关系的一侧,Client 类的实例可能有一个对象集合属性(也称为“backref”),可用于遍历其所有子 Order

让我们定义映射模式以及 ClientOrder 两个类。

#include "orm/domain_object.h"
#include "orm/domain_factory.h"
#include "orm/schema_decl.h"
class Order;
class Client: public Yb::DomainObject {
YB_DECLARE(Client, "client_tbl", "client_seq", "client",
    YB_COL_PK(id, "id")
    YB_COL_DATA(dt, "dt", DATETIME)
    YB_COL_STR(name, "name", 100)
    YB_COL_STR(email, "email", 100)
    YB_COL_DATA(budget, "budget", DECIMAL)
    YB_REL_ONE(Client, owner, Order, orders, Yb::Relation::Restrict, "client_id", 1, 1)
    YB_COL_END)
public:
    int get_info() const { return 42; }
};
class Order: public Yb::DomainObject {
YB_DECLARE(Order, "order_tbl", "order_seq", "order",
    YB_COL_PK(id, "id")
    YB_COL_FK(client_id, "client_id", "client_tbl", "id")
    YB_COL(dt, "dt", DATETIME, 0, 0, Yb::Value("sysdate"), "", "", "", "")
    YB_COL_STR(memo, "memo", 100)
    YB_COL_DATA(total_sum, "total_sum", DECIMAL)
    YB_COL_DATA(paid_sum, "paid_sum", DECIMAL)
    YB_COL_DATA(paid_dt, "paid_dt", DATETIME)
    YB_REL_MANY(Client, owner, Order, orders, Yb::Relation::Restrict, "client_id", 1, 1)
    YB_COL_END)
public:
    const Yb::Decimal to_be_paid() {
        return total_sum - paid_sum.value(0);
    }
};

这些类声明可以放在头文件或.cpp文件中。在你的.cpp文件中还需要两句话才能实现魔力。

YB_DEFINE(Client)
YB_DEFINE(Order)

ClientOrder 类会自动获得一些新的数据成员和方法。现在每个类的实例都有映射的属性(iddtname…)。这些属性可用于读写模式访问列数据,以及检查缺失值(IS NULL)。

要控制映射类实例,必须有一个 Yb::Session 类的实例,它负责加载/保存对象、跟踪更改、控制关系等。创建 Session 时,请向其传递数据库模式。

int main() {
    Yb::init_schema();  // gather all declarations in one schema
    Yb::Session session(Yb::theSchema(), "sqlite+sqlite://./tut1.db");
    session.create_schema(true);  // create schema if necessary

现在您可以立即像这样使用领域类了。

    Order order;
    order.total_sum = Yb::Decimal("3.14");
    order.paid_sum = Yb::Decimal(0);
    order.save(session);
    Client client;
    client.name = "Some Name";
    client.email = "some@email";
    client.dt = Yb::now();
    client.save(session);
    order.owner = Client::Holder(client);
    session.commit();
    return 0;
}

您可以编译示例,链接 ybutilyborm 库,然后就可以运行了。如果您愿意,可以开启日志记录,看看后台发生了什么。

#include "util/nlogger.h"
#include <iostream>
...
    Yb::LogAppender appender(std::cerr);
    Yb::init_schema();  // gather all declarations in one schema
    Yb::Session session(Yb::theSchema(), "sqlite+sqlite://./tut1.db");
    session.set_logger(Yb::ILogger::Ptr(new Yb::Logger(&appender)));

以下是针对 SQLite 数据库引擎的特定日志消息。

14-10-27 14:19:38.489 21962/21962 DEBG sql: exec_direct: CREATE TABLE client_tbl ( 
        id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 
        dt TIMESTAMP, 
        name VARCHAR(100), 
        email VARCHAR(100), 
        budget NUMERIC 
) 
14-10-27 14:19:38.818 21962/21962 DEBG sql: exec_direct: CREATE TABLE order_tbl ( 
        id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 
        client_id INTEGER NOT NULL, 
        dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, 
        memo VARCHAR(100), 
        total_sum NUMERIC, 
        paid_sum NUMERIC, 
        paid_dt TIMESTAMP 
        , FOREIGN KEY (client_id) REFERENCES client_tbl(id) 
) 
14-10-27 14:19:38.842 21962/21962 DEBG orm: flush started 
14-10-27 14:19:38.843 21962/21962 DEBG sql: begin transaction 
14-10-27 14:19:38.843 21962/21962 DEBG sql: 
prepare: INSERT INTO client_tbl (dt, name, email, budget) VALUES (?, ?, ?, ?) 
14-10-27 14:19:38.843 21962/21962 DEBG sql: bind: (DateTime, String, String, Decimal) 
14-10-27 14:19:38.843 21962/21962 DEBG sql: exec prepared: p1="'2014-10-27 
14:19:38'" p2="'Some Name'" p3="'some@email'" p4="NULL" 
14-10-27 14:19:38.844 21962/21962 DEBG sql: 
prepare: SELECT SEQ LID FROM SQLITE_SEQUENCE WHERE NAME = 'client_tbl' 
14-10-27 14:19:38.844 21962/21962 DEBG sql: exec prepared: 
14-10-27 14:19:38.844 21962/21962 DEBG sql: fetch: LID='1' 
14-10-27 14:19:38.844 21962/21962 DEBG sql: fetch: no more rows 
14-10-27 14:19:38.845 21962/21962 DEBG sql: prepare: INSERT INTO order_tbl 
(client_id, dt, memo, total_sum, paid_sum, paid_dt) VALUES (?, ?, ?, ?, ?, ?) 
14-10-27 14:19:38.845 21962/21962 DEBG sql: bind: (LongInt, DateTime, String, Decimal, Decimal, DateTime) 
14-10-27 14:19:38.845 21962/21962 DEBG sql: exec prepared: p1="1" 
p2="'2014-10-27 14:19:38'" p3="NULL" p4="3.14" p5="0" p6="NULL" 
14-10-27 14:19:38.845 21962/21962 DEBG sql: 
prepare: SELECT SEQ LID FROM SQLITE_SEQUENCE WHERE NAME = 'order_tbl' 
14-10-27 14:19:38.846 21962/21962 DEBG sql: exec prepared: 
14-10-27 14:19:38.846 21962/21962 DEBG sql: fetch: LID='1' 
14-10-27 14:19:38.846 21962/21962 DEBG sql: fetch: no more rows 
14-10-27 14:19:38.846 21962/21962 DEBG orm: flush finished OK 
14-10-27 14:19:38.846 21962/21962 DEBG sql: commit 

请注意正确的插入顺序(先父后子)。这是通过对对象图进行拓扑排序实现的。外键的值会自动分配,主键的值也是如此。

如果我们从另一侧操作对象之间的链接,也可以达到相同的效果。

    //order.owner = Client::Holder(client); 
    client.orders.insert(order); 

领域类在构建查询方面尤其有用。例如,我们需要一个特定客户端订单的分页器,让我们获取第 30 到第 39(含)项。

#include <boost/foreach.hpp>
...
    Yb::DomainResultSet<Order> rs = Yb::query<Order>(session) 
        .filter_by(Order::c.client_id == 32738) 
        .order_by(Order::c.dt) 
        .range(30, 40).all(); 
    BOOST_FOREACH(Order order, rs) { 
        std::cout << order.id << ","; 
    } 

在这里,我们可以看到一个在不同 SQL 方言中实现方式不同的功能。例如,对于 SQLite,将发出以下 SQL 代码。

SQL:
SELECT order_tbl.id, order_tbl.client_id, order_tbl.dt, order_tbl.memo, 
 order_tbl.total_sum, order_tbl.paid_sum, order_tbl.paid_dt 
FROM order_tbl WHERE (order_tbl.client_id = ?)
ORDER BY order_tbl.dt
LIMIT ? OFFSET ?

positional params: (32738, 10, 30)

有关更多示例、下载和任何进一步信息,请访问项目主页:https://sourceforge.net/projects/yborm/

关注点

实现自己的 ORM 绝非易事。仅仅是因为内存中对象与 SQL 表的同步任务本身就很复杂。此外,在整个系统能够第一次执行 session.flush() 之前,还有许多伴随的常规问题需要解决。

2007 年,我参与的一个项目,简单的任务都要费很大力气才能解决,更复杂的根本没有解决。改用 ORM 彻底改变了这种情况。当然,学习使用任何 ORM 工具都有一个学习曲线。我希望您能从在项目中正确使用 ORM 概念中受益。

感谢阅读。欢迎反馈。

历史

  • 2014-10-30:发布 YB.ORM 0.4.6 后发布了初始修订版。
© . All rights reserved.