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

Oracle Call Interface (OCI) 和 ODP.Net - 第一部分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2020年12月3日

CPOL

29分钟阅读

viewsIcon

27920

downloadIcon

470

使用 .NET 5.0 和原生 C++ 进行快速 Oracle 数据库访问的可靠技术

引言

从客户端或服务器应用程序高效地创建、检索、更新和删除数据库中的数据通常是解决方案中最具性能关键性的部分,而根据 DB-Engines.com 的数据,Oracle 数据库是全球最受欢迎的数据库服务器。

Oracle 数据库服务器被广泛应用于以下行业的公司:

  • 电力公用事业行业
  • 石油和天然气行业
  • 金融和投资行业

我相信 Oracle 流行度的主要原因在于:

  • 可靠性
  • 容量
  • 灵活性
  • 性能

前三点是开箱即用的功能,而第四点可能有点难以实现。

想象一下,您的任务是为股票市场创建一个实时分析应用程序,交易员将根据您的应用程序提供的及时结果做出关系到公司成败的决策。

12 月 1 日纳斯达克市场的总交易量为 30,910,548 笔,其中大部分交易可能在开盘后一小时内或收盘前一小时内完成。大量的交易可能在活动高峰期集中发生,因此您的解决方案也需要能够优雅地处理这种情况。

一个典型的 .NET 应用程序,一次插入一行,每秒大约可以插入 3,500 行,这远低于您的解决方案所需。虽然将数据导入数据库非常重要,但真正目的是将应用于数据的算法。任何进入数据库的数据都将被读取数千次——一切都必须尽快执行,以便对交易台具有实际价值。

本文将演示如何在不更改数据库的情况下,每秒将超过 1,200,000 行数据插入 Oracle 数据库,并每秒读取超过 3,600,000 行数据。当涉及到性能时,您与 Oracle 数据库的通信方式至关重要。

我将解释如何使用 Oracle Call Interface

  • 执行基本的创建、检索、更新和删除(CRUD)操作,并附带乐观锁定,
  • 将数据绑定到 SQL 语句中的变量,
  • 高效地传递输入数据,
  • 高效地处理查询结果,
  • 使用单个数据库调用将 100,000,000 行数据插入表,并且
  • 使用单个数据库调用更新表中的 100,000,000 行数据

本文还演示了 ODP.Net 如何与 .NET 5.0 一起使用来执行许多相同的操作。ODP.Net 可用于实现几乎所有可以使用 OCI 实现的功能,但 OCI 的性能始终优于 ODP.Net。性能有时对应用程序的成功至关重要,对于基于云的部署,它对日常运营成本有巨大影响。本文将解释如何使用 ODP.Net 来

  • 执行基本的创建、检索、更新和删除(CRUD)操作,并附带乐观锁定,
  • 将数据绑定到 SQL 语句中的变量,
  • 高效地传递输入数据,
  • 使用单个数据库调用将 1,000,000 行数据插入表,并且
  • 使用单个数据库调用更新表中的 1,000,000 行数据

Oracle Call Interface (OCI) 是一个用于使用 C 或 C++ 编写与 Oracle 数据库交互的应用程序的 API。通过使用 OCI API,我们可以访问 Oracle 数据库提供的所有数据库操作,包括 SQL 语句处理和对象操作。

与其他访问 Oracle 数据库的方法相比,Oracle Call Interface 提供了显著的优势:

  • 对应用程序设计和程序执行的所有方面进行更细粒度的控制
  • 更快的连接池、会话池和语句缓存,能够开发出经济高效且高度可扩展的应用程序
  • 更高效地执行动态 SQL
  • 使用回调函数进行动态绑定和定义
  • 广泛的描述功能,用于公开服务器元数据的层
  • 为已注册的客户端应用程序提供高效的异步事件通知
  • 增强的数组数据操作语言(DML)功能,用于数组插入、更新和删除
  • 通过透明预取缓冲区优化查询,以减少往返次数
  • 能够将提交请求与执行关联,以减少服务器往返次数
  • 数据类型映射和操作函数,用于操作 Oracle 类型的属性数据
  • 数据加载函数,用于直接将数据加载到数据库中,而无需使用 SQL 语句
  • 外部过程函数,用于从 PL/SQL 编写 C 回调

根据 Oracle 的说法::

Oracle Call Interface (OCI) 是为自定义或打包应用程序提供的、全面、高性能的原生 C 语言接口,用于访问 Oracle 数据库。

OCI 高度可靠。SQL*Plus、Real Application Testing (RAT)、SQL*Loader 和 Data-Pump 等 Oracle 工具都使用 OCI。OCI 为其他语言特定的接口(如 Oracle JDBC-OCI、Oracle Data Provider for Net (ODP.Net)、Oracle Precompilers、Oracle ODBC 和 Oracle C++ Call Interface (OCCI) 驱动程序)提供了基础。Node.js 的 node-oracledb、PHP OCI8、ruby-oci8、Perl DBD::Oracle、Python cx_Oracle 以及统计编程语言 R 的 ROracle 驱动程序等主流脚本语言驱动程序也使用 OCI。

为了更轻松地使用 Oracle Call Interface (OCI) API,我编写了 Harlinn.OCI C++ 库,并将其打包为 Windows DLL。

构建代码

$(SolutionDir)Readme 文件夹中的 Build.md 文件中提供了构建代码的说明。

Harlinn.OCI

使用 Oracle Call Interface 和 C++ 的主要优势是性能。这种优势并非仅仅因为您使用 C++ 和 OCI,而是可以通过深思熟虑的设计来实现。通过 Harlinn.OCI 库,我试图实现两个目标:

  1. 易用性
  2. 对通过 Oracle Call Interface 与 Oracle RDBMS 交换数据的方式进行细粒度控制。这可以极大地提高 OCI 客户端应用程序的性能。

易用性

使用 C# 和 ODP.Net,我们可以轻松连接到 Oracle RDBMS。

static void Main(string[] args)
{
    var connectionString = GetConnectionString(args);
    var connection = new OracleConnection(connectionString);
    using (connection)
    {
        connection.Open();
        using (var command = connection.CreateCommand())
        {
            command.CommandText = "SELECT * FROM ALL_USERS";
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    var userName = reader.GetString(0);
                    Console.Out.WriteLine(userName);
                }
            }
        }
    }
}

Harlinn.OCI 提供了一个您会发现与 ODP.Net 一样易于使用的 API。

EnvironmentOptions options;
Environment environment( options );
auto server = environment.CreateServer( );
auto serviceContext = server.CreateServiceContext( Username, Password, Alias );

serviceContext.SessionBegin( );

std::wstring sql =
    L"SELECT * FROM ALL_USERS";

auto statement = serviceContext.CreateStatement( sql );
auto reader = statement.ExecuteReader<DataReader>( );

while ( reader->Read( ) )
{
    auto userName = reader->As<std::wstring>( 0 );
    auto userId = reader->As<Int64>( 1 );
    auto created = reader->As<DateTime>( 2 );
}

serviceContext.SessionEnd( );

调用服务器端函数

std::wstring sql =
    L"BEGIN"\
    L" :result := SYSTIMESTAMP();" \
    L"END;";
   
auto statement = serviceContext.CreateStatement( sql );
auto result = statement.Bind<DateTime>( 1 );

statement.Execute( 1 );

auto dateTime = result->As<DateTime>( );

或插入一行

std::optional<std::wstring> description;
if ( ownedObjectType.Description( ).length( ) )
{
    description = ownedObjectType.Description( );
}

constexpr wchar_t SQL[] = L"INSERT INTO OwnedObjectType(Id, Name, OptimisticLock, "
    L"Created, Description) "\
    L"VALUES(:1,:2,0,:3,:4)";

static std::wstring sql( SQL );
auto statement = serviceContext_.CreateStatement( sql,
    ownedObjectType.Id( ),
    ownedObjectType.Name( ),
    ownedObjectType.Created( ),
    description );

statement.Execute( );

都可以使用该库轻松实现。Harlinn.OCI 在 OCI C API 上实现了一个精简但功能丰富的层。

Harlinn.OCI 依赖于 Harlinn.Common.Core 库,用于实现诸如 GuidDateTimeTimeSpan 等基本数据类型。

创建、检索、更新和删除(CRUD)

基本的 CRUD 操作是许多应用程序的核心,这是一个简单但典型的表:

CREATE TABLE SimpleTest1
(
Id NUMBER(19) NOT NULL,
OptimisticLock NUMBER(19) DEFAULT 0 NOT NULL,
Name NVARCHAR2(128) NOT NULL,
Description NVARCHAR2(1024),
CONSTRAINT PK_SimpleTest1 PRIMARY KEY(Id),
CONSTRAINT UNQ_SimpleTest1 UNIQUE(Name)
);

描述

  1. Id 列是该表的主键,要求每行数据都必须存储一个唯一的值。
  2. OptimisticLock 列用于实现乐观锁定,这是一种广泛使用的技术,用于防止多个并发用户意外覆盖数据。如果一个应用程序可以执行以下操作:
    1. User1 检索一行数据。
    2. User2 检索同一行数据。
    3. User2 更新列值并更新数据库中的行。
    4. User1 更新同一列值并更新数据库中的行,覆盖了 User2 所做的更改。

那么这几乎总是设计上的一个缺陷。

  1. Name 列为表中的行提供了一个备用键。
  2. Description 列包含解决方案在数据库中管理其数据时感兴趣的数据。

数值主键通常使用 Oracle 数据库的序列对象生成:

CREATE SEQUENCE SimpleTest1Seq;

这可以确保为多个数据库客户端应用程序的并发插入创建唯一键。

乐观锁定

几乎所有使用数据库服务器存储数据的软件解决方案都必须能够处理多个并发会话。在任何给定时间点,您都可以预期多个进程或用户正在检索和更新数据库。由于多个进程或用户正在更新数据库中的信息,因此不可避免地会出现两个不同的进程或用户尝试同时更新同一数据的情况。

乐观锁定是一种最小化的策略,用于防止基于行中某个列存储的版本号而对行进行的无意更新。当软件尝试更新或删除行时,会根据版本号进行过滤,以确保该行在从数据库检索该行之间未被更新。更新必须确保版本号列的更改是原子的。

之所以称为乐观锁定,是因为它假定大多数更新和删除都能成功;当它们不成功时,软件必须能够适当地处理。

如何正确处理乐观锁定阻止了 updatedelete 的情况,取决于用例。交互式解决方案可以从数据库中检索更新后的行,让用户决定是覆盖它还是放弃其修改;而自动化系统可以实现更复杂的解决方案,或将拒绝的更新存储在别处以供手动干预。重要的是在确保信息不丢失的情况下保持数据一致性。

乐观锁定对于高吞吐量的解决方案、Web 以及其他多层架构尤其有用,在这些架构中,软件无法为用户维护与数据库的专用连接。在这些情况下,客户端无法维护数据库锁,因为连接是从池中获取的,并且客户端在一次服务器请求与下一次请求之间可能不会使用相同的连接。

乐观锁定的主要缺点是它面向行,而许多实际解决方案需要超出单行的同步。

即使您最终使用更强大的锁定管理解决方案,乐观锁定也几乎肯定有助于在开发、部署和运行期间发现编程错误。

乐观锁定的替代方案称为悲观锁定,它**不是**“最后写入获胜”。悲观锁定需要一个主动的系统组件来维护锁,例如数据库服务器实现的表或行级锁定,或者专用的分布式锁服务器。数据库锁通常与数据库会话相关联,但也可以由分布式事务管理器控制。

Oracle 数据库提供了 DBMS_LOCK 包,可用于实现悲观锁定。锁的最大生命周期仅限于创建它的会话的生命周期。

使用 ODP.Net 进行基本 CRUD 操作

使用 ODP.Net 实现基本 CRUD 操作是一个直接的过程,代码演示了如何使用纯 DML 中的原子操作来实现乐观锁定。

完整代码位于 DotNet\Examples\ODP\Harlinn.Examples.ODP.Basics01

Create

要向数据库插入一条新记录,我们必须执行以下步骤:

  • 创建一个 OracleCommand 对象。
  • 将带有变量占位符的 INSERT 数据操作语言 (DML) 语句分配给 OracleCommand 对象的 CommandText 属性。
  • 创建 OracleParameter 对象并将其绑定到每个变量占位符。
  • 调用 OracleCommand 对象的 ExecuteNonQuery() 成员函数在数据库服务器上执行 DML 语句。

前两个占位符 :1:2 分别用于 NameDescription 列;而第三个占位符用于由调用 SimpleTest1Seq.NextVal 生成的 Id 列的值,如后面“RETURNING Id INTO :3”所示。OptimisticLock 列被赋值为 0,表示这是该行的初始版本。

public SimpleTest1Data Insert(string name, string description = null)
{
    using (var command = _connection.CreateCommand())
    {
        command.CommandText = "INSERT INTO SimpleTest1
                              (Id, OptimisticLock, Name, Description ) "
                            + "VALUES(SimpleTest1Seq.NextVal,0,:1,:2) RETURNING Id INTO :3";

        OracleParameter nameParameter = new OracleParameter();
        nameParameter.OracleDbType = OracleDbType.NVarchar2;
        nameParameter.Value = name;

        OracleParameter descriptionParameter = new OracleParameter();
        descriptionParameter.OracleDbType = OracleDbType.NVarchar2;
        descriptionParameter.Value = string.IsNullOrWhiteSpace(description) ? 
                                      DBNull.Value : description;

        OracleParameter idParameter = new OracleParameter();
        idParameter.OracleDbType = OracleDbType.Int64;

        command.Parameters.Add(nameParameter);
        command.Parameters.Add(descriptionParameter);
        command.Parameters.Add(idParameter);

        command.ExecuteNonQuery();

        var idValue  = (OracleDecimal)idParameter.Value;
        var result = new SimpleTest1Data(idValue.ToInt64(), name, description );
        return result;
    }
}

检索

准备 OracleCommand 对象以检索特定行数据的步骤几乎与我们用于插入新记录的步骤相同,只是这次我们必须:

  • 将带有单个变量占位符的结构化查询语言 (SQL) 语句分配给 OracleCommand 对象的 CommandText 属性。
  • WHERE 子句中 :1 占位符的值绑定到我们正在查找的行的 Id 列的值。
  • 调用 OracleCommand 对象的 ExecuteReader(…) 来在数据库服务器上执行查询。查询结果通过 ExecuteReader(…) 返回的 OracleDataReader 对象提供给客户端应用程序。

由于我们已经知道了 Id 列的值,我们只查询 OptimisticLockNameDescription 列。Id 列的值保证能唯一标识 SimpleTest1 表中的一行,因此我们只执行一次 reader.Read(),如果它返回 true,我们就知道查询成功找到了所需的行。OptimisticLockName 列不能为 NULL,而 Description 列可以为 NULL——我们通过调用 IsDBNull 来检查。当前行的每一列都由其在 select-list 中的 0 基偏移量标识。

public SimpleTest1Data Select(long id)
{
    using (var command = _connection.CreateCommand())
    {
        command.CommandText = "SELECT OptimisticLock, Name, Description FROM SimpleTest1 "+
                                "WHERE Id = :1";

        OracleParameter idParameter = new OracleParameter();
        idParameter.OracleDbType = OracleDbType.Int64;
        idParameter.Value = id;

        command.Parameters.Add(idParameter);

        using (var reader = command.ExecuteReader(System.Data.CommandBehavior.SingleRow))
        {
            if (reader.Read())
            {
                var optimisticLock = reader.GetInt64(0);
                var name = reader.GetString(1);
                string description = null;
                if (reader.IsDBNull(2) == false)
                {
                    description = reader.GetString(2);
                }
                var result = new SimpleTest1Data(id, optimisticLock, name, description);
                return result;
            }
            else
            {
                return null;
            }
        }
    }
}

更新

同样,我们使用 OracleCommand 对象,将 UPDATE DML 语句分配给 CommandText 属性。这次,变量占位符仅用于输入变量。

只有当 Id 列与绑定到第三个变量占位符的值匹配,并且 OptimisticLock 列与绑定到第四个变量占位符的值匹配时,该行才会被更新。这将阻止 DML 更新该行,如果它已被另一个用户或进程更新;并且作为副作用,我们也知道在成功更新后,OptimisticLock 列的下一个值将是前一个值加 1。

DML 语句最多可以更新 SimpleTest1 表中的一行,并且由于 ExecuteNonQuery() 返回被 DML 语句修改的行数,我们可以安全地假设返回值大于 0 表示更新成功,而 0 表示该行已被删除或 OptimisticLock 列的值已被其他更新更改。

public bool Update(SimpleTest1Data data)
{
    using (var command = _connection.CreateCommand())
    {
        command.CommandText = "UPDATE SimpleTest1 "+
                        "SET OptimisticLock=OptimisticLock+1, Name=:1, Description=:2 "+
                        "WHERE Id=:3 AND OptimisticLock=:4";

        var id = data.Id;
        var optimisticLock = data.OptimisticLock;
        var name = data.Name;
        var description = data.Description;

        OracleParameter nameParameter = new OracleParameter();
        nameParameter.OracleDbType = OracleDbType.NVarchar2;
        nameParameter.Value = name;

        OracleParameter descriptionParameter = new OracleParameter();
        descriptionParameter.OracleDbType = OracleDbType.NVarchar2;
        descriptionParameter.Value = string.IsNullOrWhiteSpace(description) ? 
                                                     DBNull.Value : description;

        OracleParameter idParameter = new OracleParameter();
        idParameter.OracleDbType = OracleDbType.Int64;
        idParameter.Value = id;

        OracleParameter optimisticLockParameter = new OracleParameter();
        optimisticLockParameter.OracleDbType = OracleDbType.Int64;
        optimisticLockParameter.Value = optimisticLock;

        command.Parameters.Add(nameParameter);
        command.Parameters.Add(descriptionParameter);
        command.Parameters.Add(idParameter);
        command.Parameters.Add(optimisticLockParameter);

        if (command.ExecuteNonQuery() > 0)
        {
            data.OptimisticLock = optimisticLock + 1;
            return true;
        }
        else
        {
            return false;
        }
    }
}

删除

DELETE DML 语句的逻辑与更新行的逻辑几乎相同。

public bool Delete(SimpleTest1Data data)
{
    using (var command = _connection.CreateCommand())
    {
        command.CommandText = "DELETE FROM SimpleTest1 " +
                                "WHERE Id=:1 AND OptimisticLock=:2";

        var id = data.Id;
        var optimisticLock = data.OptimisticLock;

        OracleParameter idParameter = new OracleParameter();
        idParameter.OracleDbType = OracleDbType.Int64;
        idParameter.Value = id;

        OracleParameter optimisticLockParameter = new OracleParameter();
        optimisticLockParameter.OracleDbType = OracleDbType.Int64;
        optimisticLockParameter.Value = optimisticLock;

        command.Parameters.Add(idParameter);
        command.Parameters.Add(optimisticLockParameter);

        if (command.ExecuteNonQuery() > 0)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}

一个简单的测试

为了将这些内容整合起来,下面的示例在数据库中创建了 100 行数据,检索存储的行并验证它们包含预期的数据。然后删除其中三分之一的行,期望每次调用 Delete(…) 返回 true——表示删除操作成功,然后再次对相同的对象调用 Delete(…)。这次,期望每次调用都返回 false,表示该行已被第一轮遍历对象的操作删除。

public void Execute()
{
    Clear();
    var originalItems = new Dictionary<long, SimpleTest1Data>( );
    int RowCount = 100;
    using (var transaction = _connection.BeginTransaction())
    {
        for (int i = 0; i < RowCount; ++i)
        {
            var name = $"Name{i + 1}";
            var description = i % 2 == 0 ? null : $"Description{i + 1}";
            var data = Insert(name, description);
            originalItems.Add(data.Id, data);
        }
        transaction.Commit();
    }

    var databaseItems = Select();

    foreach (var entry in originalItems)
    {
        var originalItem = entry.Value;
        if (databaseItems.ContainsKey(originalItem.Id))
        {
            var databaseItem = databaseItems[originalItem.Id];
            if (originalItem.Equals(databaseItem) == false)
            {
                throw new Exception($"The original item {originalItem} "+
                    $"is not equal to the item {databaseItem} "+
                    "retrieved from the database.");
            }
        }
        else
        {
            throw new Exception($"Did not retrieve {originalItem} from "+
                "the database.");
        }
    }

    using (var transaction = _connection.BeginTransaction())
    {
        foreach (var entry in originalItems)
        {
            var data = entry.Value;
            if (string.IsNullOrWhiteSpace(data.Description))
            {
                data.Description = "Updated Description";
                if (Update(data) == false)
                {
                    var changedData = Select(data.Id);
                    if (changedData != null)
                    {
                        throw new Exception($"Unable to update {data}, the "+
                            $"row has been updated by another user {changedData}");
                    }
                    else
                    {
                        throw new Exception($"Unable to update {data}, the "+
                            $"row has been deleted by another user");
                    }
                }
            }
        }
        transaction.Commit();
    }

    int rowsToDeleteCount = RowCount/3;
    var itemsToDelete = originalItems.Values.Take(rowsToDeleteCount).ToList();

    using (var transaction = _connection.BeginTransaction())
    {
        foreach (var item in itemsToDelete)
        {
            if (Delete(item) == false)
            {
                throw new Exception($"Unable to delete {item}, the row has "
                    +"been deleted by another user");
            }
        }
        transaction.Commit();
    }

    foreach (var item in itemsToDelete)
    {
        if ( Delete(item) )
        {
            throw new Exception($"It appears {item}, was not deleted");
        }
    }
}

使用 Harlinn.OCI 进行基本 CRUD 操作

使用 Harlinn.OCI 实现基本 CRUD 操作也是一个直接的过程,代码再次演示了如何使用纯 DML 中的原子操作正确实现乐观锁定。

完整代码位于 Examples\OCI\HOCIBasics01

Create

要向数据库插入一条新记录,我们必须执行以下步骤:

  • 通过调用服务上下文上的 CreateStatement(…) 来创建一个 OCI::Statement 对象。第一个参数是带有 Name、Description 和最终服务器生成的 Id 列值的变量占位符的 INSERT 数据操作语言 (DML) 语句。第二个和第三个参数会自动绑定到前两个变量占位符。
  • 使用 Int64Bind 对象将第三个变量占位符绑定到一个 64 位整数,该整数将接收服务器生成的主键。
  • 调用 OCI::Statement 对象的 ExecuteNonQuery() 成员函数在数据库服务器上执行 DML 语句。

前两个占位符 :1:2 分别用于 NameDescription 列;而第三个占位符用于由调用 SimpleTest1Seq.NextVal 生成的 Id 列的值,如后面“RETURNING Id INTO :3”所示。OptimisticLock 列被赋值为 0,表示这是该行的初始版本。

std::unique_ptr<SimpleTestData> Insert( const std::wstring& name,
                    const std::wstring& description = std::wstring( ) ) const
{
    auto& serviceContext = ServiceContext( );
    std::optional<std::wstring> descr;
    if ( description.size( ) )
    {
        descr = description;
    }

    constexpr wchar_t sql[] =
        L"INSERT INTO SimpleTest1(Id, OptimisticLock, Name, Description ) "
        L"VALUES(SimpleTest1Seq.NextVal,0,:1,:2) RETURNING Id INTO :3";

    auto statement = serviceContext.CreateStatement( sql, name, descr );
    auto* idBind = statement.Bind<Int64>( 3 );
    auto transaction = serviceContext.BeginTransaction( );
    statement.ExecuteNonQuery( );
    transaction.Commit( );
    auto id = idBind->AsInt64( );
    return std::make_unique<SimpleTestData>( id, 0, name, description );
}

检索

准备 OCI::Statement 对象以检索特定行数据的步骤几乎与插入新记录时使用的步骤相同,只是这次我们必须:

  • 将带有单个变量占位符的结构化查询语言 (SQL) 语句作为第一个参数传递给 CreateStatement(…) 函数,并将要赋给此占位符的值作为第二个参数。
  • 调用 OCI::Statement 对象的 ExecuteReader 在数据库服务器上执行查询。ExecuteReader 返回指定类型的 OCI::DataReader,该对象提供对查询结果的访问。

由于我们已经知道了 Id 列的值,我们只查询 OptimisticLockNameDescription 列。Id 列的值保证能唯一标识 SimpleTest1 表中的一行,因此我们只执行一次 reader->Read(),如果它返回 true,我们就知道查询成功找到了所需的行。OptimisticLockName 列不能为 NULL,而 Description 列可以为 NULL——我们通过调用 IsDBNull 来检查。当前行的每一列都由其在 select-list 中的 0 基偏移量标识。

std::unique_ptr<SimpleTestData> Select( Int64 id ) const
{
    auto& serviceContext = ServiceContext( );
    constexpr wchar_t sql[] =
        L"SELECT OptimisticLock, Name, Description FROM SimpleTest1 "
        L"WHERE Id = :1";

    auto statement = serviceContext.CreateStatement( sql, id );
    auto reader = statement.ExecuteReader<OCI::DataReader>( );
    if ( reader->Read( ) )
    {
        auto optimisticLock = reader->GetInt64( 0 );
        auto name = reader->GetString( 1 );
        std::wstring description;
        if ( reader->IsDBNull( 2 ) == false )
        {
            description = reader->GetString( 2 );
        }
        return std::make_unique<SimpleTestData>( id, optimisticLock,
            name, description );
    }
    else
    {
        return nullptr;
    }
}


更新

同样,我们使用 OCI::Statement 对象,将 UPDATE DML 语句作为第一个参数传递给 CreateStatement(…) 函数。这次,变量占位符仅用于输入变量,而保存值的变量作为附加参数传递。

只有当 Id 列与绑定到第三个变量占位符的值匹配,并且 OptimisticLock 列与绑定到第四个变量占位符的值匹配时,该行才会被更新。这将阻止 DML 更新该行,如果它已被另一个用户或进程更新;并且作为副作用,我们也知道在成功更新后,OptimisticLock 列的下一个值将是前一个值加上 1

DML 语句最多可以更新 SimpleTest1 表中的一行,并且由于 ExecuteNonQuery() 返回被 DML 语句修改的行数,我们可以安全地假设返回值大于 0 表示更新成功,而 0 表示该行已被删除或 OptimisticLock 列的值已被其他更新更改。

bool Update( SimpleTestData& data )
{
    auto id = data.Id();
    auto optimisticLock = data.OptimisticLock();
    auto& name = data.Name();
    auto& description = data.Description();

    auto& serviceContext = ServiceContext( );
    std::optional<std::wstring> descr;
    if ( description.size( ) )
    {
        descr = description;
    }
    constexpr wchar_t sql[] = L"UPDATE SimpleTest1 "
        L"SET OptimisticLock=OptimisticLock+1, Name=:1, Description=:2 "
        L"WHERE Id=:3 AND OptimisticLock=:4";

    auto statement = serviceContext.CreateStatement( sql, name, descr,
                                    id, optimisticLock );
    if ( statement.ExecuteNonQuery( ) > 0 )
    {
        data.SetOptimisticLock( optimisticLock + 1 );
        return true;
    }
    else
    {
        return false;
    }
}

删除

DELETE DML 语句的逻辑与更新行的逻辑几乎相同。

bool Delete( const SimpleTestData& data )
{
    auto id = data.Id( );
    auto optimisticLock = data.OptimisticLock( );
    auto& serviceContext = ServiceContext( );

    constexpr wchar_t sql[] = L"DELETE FROM SimpleTest1 "
        L"WHERE Id=:1 AND OptimisticLock=:2";

    auto statement = serviceContext.CreateStatement( sql, id, optimisticLock );
    if ( statement.ExecuteNonQuery( ) > 0 )
    {
        return true;
    }
    else
    {
        return false;
    }
}

OCI 程序初始化

几乎所有可以通过 OCI 完成的操作都是通过 OCI 资源的句柄来执行的。每个使用 OCI 的应用程序都必须创建一个 OCI 环境句柄,从而定义执行 OCI 函数的上下文。环境句柄建立了用于快速内存访问的内存缓存,环境使用的所有内存都来自此缓存。

环境

Environment 类提供了对 OCI 环境句柄功能的访问。

OCI 应用程序使用错误句柄作为客户端应用程序和 API 之间错误信息的通道,而 ErrorHandle 类提供了此句柄类型的相关功能。

创建 Environment 是使用 Harlinn.OCI 库创建应用程序时的第一步。

EnvironmentOptions options;
Environment environment( options );

Environment 构造函数调用 CreateEnvironment( ),它创建对象的句柄。

void* Environment::CreateEnvironment( )
{
    void* handle = nullptr;
    auto rc = OCIEnvCreate( (OCIEnv**)&handle,
        (UInt32)DefaultEnvironmentMode( ),
        nullptr, nullptr, nullptr, nullptr, (size_t)0, (dvoid**)0 );

    if ( rc < OCI::Result::Success )
    {
        ThrowOracleExceptionOnError( handle, rc );
    }
    return handle;
}

ErrorHandle

一旦 Environment 对象拥有有效句柄,它就会创建一个 ErrorHandle 对象,该对象用于处理与此 Environment 对象相关的所有错误,但调用 OCIHandleAlloc 除外。

OCI::ErrorHandle Environment::CreateError( ) const
{
    void* errorHandle = nullptr;

    auto rc = OCIHandleAlloc( (dvoid*)Handle( ),
        (dvoid**)&errorHandle,
        OCI_HTYPE_ERROR, 0, (dvoid**)0 );

    if ( rc < OCI_SUCCESS )
    {
        ThrowOracleExceptionOnError( Handle( ), rc );
    }
    return OCI::ErrorHandle( *this, errorHandle, true );
}

ServiceContext、Server 和 Session

接下来,我们需要建立一个服务上下文句柄,该句柄是大多数 OCI 操作调用所必需的。

服务上下文句柄包含三个句柄,分别代表服务器连接、用户会话和事务。

  • 服务器句柄代表客户端和数据库服务器之间面向连接的传输机制中的物理连接。
  • 用户会话定义了用户的角色和权限。
  • 事务句柄代表用于对服务器执行操作的事务上下文。这包括用户会话状态信息,例如提取状态和包实例化。

要建立一个服务上下文句柄,可以使用该句柄与 Oracle 数据库执行 SQL 语句,我们需要执行一系列步骤:

  1. 使用 OCIHandleAlloc 分配服务器句柄。
  2. 使用 OCIServerAttach 初始化服务器句柄。
  3. 使用 OCIHandleAlloc 分配服务上下文句柄。
  4. 使用 OCIAttrSet 将服务器句柄分配给服务上下文句柄。
  5. 使用 OCIHandleAlloc 分配用户会话句柄。
  6. 使用 OCIAttrSet 将用户会话句柄分配给服务上下文。
  7. 使用 OCIAttrSet 将用户名分配给用户会话。
  8. 使用 OCIAttrSet 将密码分配给用户会话。
  9. 使用 OCISessionBegin 初始化服务上下文。

Harlinn.OCI 将此简化为:

auto server = environment.CreateServer( );
auto serviceContext = server.CreateServiceContext( Username, Password, Alias );
serviceContext.SessionBegin( );

但也允许单独执行每一步:

auto server = environment.CreateServer( );
server.Attach( Alias );
auto serviceContext = environment.CreateServiceContext( );
serviceContext.SetServer( server );
auto session = environment.CreateSession( );
serviceContext.SetSession( std::move( session ) );
session.SetUserName( Username );
session.SetPassword( Password );
serviceContext.SessionBegin( );

这在您需要更好地控制如何配置 OCI 提供的各种句柄类型的选项时非常有用。

执行 Oracle 数据库的 SQL

现在,我们已经建立了有效的服务上下文,就可以开始在 Oracle 数据库上执行 SQL 语句了。要使用 OCI 执行 SQL 语句,客户端应用程序需要执行以下步骤:

  1. 使用 OCIStmtPrepare2() 分配 SQL 语句的语句句柄。
  2. 对于包含输入或输出变量的语句,必须使用 OCIBindByPos2()OCIBindByName2()OCIBindObject()OCIBindDynamic()OCIBindArrayOfStruct() 将每个占位符绑定到客户端应用程序中的地址。
  3. 通过调用 OCIStmtExecute() 来执行语句。

其余步骤仅对 SQL 查询是必需的:

  1. 使用 OCIParamGet()OCIAttrGet() 描述 select-list 项。如果 select-list 的元素在编译时已知,则此步骤不是必需的。
  2. 为 select 列表中的每个项使用 OCIDefineByPos2()OCIDefineByPos()OCIDefineObject()OCIDefineDynamic()OCIDefineArrayOfStruct() 定义输出变量。
  3. 使用 OCIStmtFetch2() 获取查询结果。

下面的代码显示了使用绑定输入变量执行 SQL 查询的最简单方法:

std::wstring sql =
    L"SELECT * FROM ALL_USERS WHERE USERNAME<>:1";

std::wstring myName( L"ESPEN" );
auto statement = serviceContext.CreateStatement( sql, myName );
auto reader = statement.ExecuteReader<DataReader>( );
while ( reader->Read( ) )
{
    auto userName = reader->As<std::wstring>( 0 );
    auto userId = reader->As<Int64>( 1 );
    auto created = reader->As<DateTime>( 2 );
}

CreateStatement(…) 函数会自动绑定除第一个参数外的所有参数,并能够为以下 C++ 类型执行此操作:

  • boolstd::optional<bool>
  • SByte (signed char) 和 std::optional<SByte>
  • Byte (unsigned char) 和 std::optional<Byte>
  • Int16 (short) 和 std::optional<Int16>
  • UInt16 (unsigned short) 和 std::optional<UInt16>
  • Int32 (int) 和 std::optional<Int32>
  • UInt32 (unsigned int) 和 std::optional<UInt32>
  • Int64 (long long) 和 std::optional<Int64>
  • UInt64 (unsigned long long) 和 std::optional<UInt64>
  • Single (float) 和 std::optional<Single>
  • Double (double) 和 std::optional<Double>
  • DateTimestd::optional<DateTime>
  • Guidstd::optional<Guid>
  • std::wstringstd::optional<std::wstring>

如果参数作为受支持的 std::optional<> 类型之一传递,则使用 std::optional<>::has_value() 来控制绑定的 NULL 指示符。

这种绑定变量的方式仅适用于输入变量。CreateStatement 函数实现为可变参数模板函数,因此它能够根据参数的类型绑定参数。

template<typename ...BindableTypes>
inline OCI::Statement ServiceContext::CreateStatement( const std::wstring& sql,
    BindableTypes&& ...bindableArgs ) const
{
    auto result = CreateStatement( sql );
    Internal::BindArgs( result, 1,
        std::forward<BindableTypes>( bindableArgs )... );
    return result;
}

CreateStatement( sql ) 仅调用 OCIStmtPrepare2 并检查错误。

OCI::Statement ServiceContext::CreateStatement( const std::wstring& sql ) const
{
    auto& environment = Environment( );
    if ( environment.IsValid( ) )
    {
        OCIStmt* ociStatement = nullptr;
        auto& error = Error( );
        auto errorHandle = (OCIError*)error.Handle( );
        auto rc = OCIStmtPrepare2( (OCISvcCtx*)Handle( ), &ociStatement, errorHandle,
            (OraText*)sql.c_str( ), static_cast<UInt32>( sql.length( ) * sizeof( wchar_t ) ),
            nullptr, 0, OCI_NTV_SYNTAX, OCI_DEFAULT );
        error.CheckResult( rc );
        return Statement( *this, ociStatement, true );
    }
    else
    {
        ThrowInvalidEnvironment( );
    }
}

BindArgs 的内部实现则处理每个可变参数模板参数。

template<typename Arg, typename ...OtherArgsTypes>
void BindArgs( OCI::Statement& statement, UInt32 position,
    const Arg& arg, OtherArgsTypes&& ...otherArgsTypes )
{
    if constexpr ( IsAnyOf<Arg, std::wstring> )
    {
        auto newBind = statement.Bind<Arg>( position, arg.length( ) );
        newBind->Assign( arg );
    }
    else if constexpr ( IsSpecializationOf<Arg, std::optional> )
    {
        using BintT = typename Arg::value_type;
        if ( arg.has_value( ) )
        {
            if constexpr ( IsAnyOf< BintT, std::wstring> )
            {
                auto newBind = statement.Bind<BintT>( position, arg.value( ).length( ) );
                newBind->Assign( arg.value() );
            }
            else
            {
                auto newBind = statement.Bind<BintT>( position );
                newBind->Assign( arg.value( ) );
            }
        }
        else
        {
            if constexpr ( IsAnyOf<BintT, std::wstring> )
            {
                auto newBind = statement.Bind<BintT>( position, static_cast<size_t>(0) );
                newBind->SetDBNull( );
            }
            else
            {
                auto newBind = statement.Bind<BintT>( position );
                newBind->SetDBNull( );
            }
        }
    }
    else
    {
        auto newBind = statement.Bind<Arg>( position );
        newBind->Assign( arg );
    }
    if constexpr ( sizeof...( otherArgsTypes ) > 0 )
    {
        BindArgs( statement, position + 1, std::forward<OtherArgsTypes>( otherArgsTypes )... );
    }
}

这是一个 C++ 如何执行复杂的编译时逻辑的示例,同时确保代码仍然可以调试,其中使用了“if constexpr”来控制代码生成。在 C++17 之前,调试涉及编译时逻辑的代码可能会相当令人困惑。

ExecuteReader 函数是所有魔术发生的地方,到目前为止唯一调用的 OCI 函数是 OCIStmtPrepare2

auto reader = statement.ExecuteReader<DataReader>( );

ExecuteReader 执行三个有趣的操作:

  1. 创建 DataReader 对象,或派生自 DataReader 的类型的对象。
  2. 在新创建的对象上调用 InitializeDefines() 函数。
  3. 执行 SQL 语句。
template<typename DataReaderType>
    requires std::is_base_of_v<OCI::DataReader, DataReaderType>
inline std::unique_ptr<DataReaderType> Statement::ExecuteReader( 
                                       StatementExecuteMode executeMode )
{
    auto result = std::make_unique<DataReaderType>( *this );
    result->InitializeDefines( );
    auto rc = Execute( 1, executeMode );
    result->Prefetch( rc );
    return result;
}

DataReader 提供了 InitializeDefines() 的默认实现,该实现执行显式描述以确定 select-list 的字段,并使用 OCIDefineByPos2 创建适当的定义。

另一种选择是创建一个派生自 DataReader 的类。

class AllUsersReader : public DataReader
{
public:
    using Base = DataReader;
    constexpr static UInt32 USERNAME = 0;
    constexpr static UInt32 USER_ID = 1;
    constexpr static UInt32 CREATED = 2;
    constexpr static UInt32 COMMON = 3;
    constexpr static UInt32 ORACLE_MAINTAINED = 4;
    constexpr static UInt32 INHERITED = 5;
    constexpr static UInt32 DEFAULT_COLLATION = 6;
    constexpr static UInt32 IMPLICIT = 7;
    constexpr static UInt32 ALL_SHARD = 8;

    constexpr static wchar_t SQL[] = L"SELECT USERNAME, "
        L"USER_ID, CREATED, COMMON, ORACLE_MAINTAINED, "
        L"INHERITED, DEFAULT_COLLATION, IMPLICIT, ALL_SHARD "
        L"FROM ALL_USERS";
    ...
};

每个字段都有自己的 ID,即 select-list 中的偏移量。由于我们知道定义(defines)的类型,我们可以为每个字段创建成员变量:

private:
    CStringDefine* userName_ = nullptr;
    Int64Define* userId_ = nullptr;
    DateDefine* created_ = nullptr;
    CStringDefine* common_ = nullptr;
    CStringDefine* oracleMaintained_ = nullptr;
    CStringDefine* inherited_ = nullptr;
    CStringDefine* defaultCollation_ = nullptr;
    CStringDefine* implicit_ = nullptr;
    CStringDefine* allShard_ = nullptr;
public:

然后重写 InitializeDefines( ) 函数:

virtual void InitializeDefines( ) override
{
    userName_ = Define<CStringDefine>( USERNAME + 1, 128 );
    userId_ = Define<Int64Define>( USER_ID + 1 );
    created_ = Define<DateDefine>( CREATED + 1 );
    common_ = Define<CStringDefine>( COMMON + 1, 3 );
    oracleMaintained_ = Define<CStringDefine>( ORACLE_MAINTAINED + 1, 1 );
    inherited_ = Define<CStringDefine>( INHERITED + 1, 3 );
    defaultCollation_ = Define<CStringDefine>( DEFAULT_COLLATION + 1, 100 );
    implicit_ = Define<CStringDefine>( IMPLICIT + 1, 3 );
    allShard_ = Define<CStringDefine>( ALL_SHARD + 1, 3 );
}

这消除了对 select-list 进行任何描述的需要,并提供了对通过 OCI 获取的数据的对象(接收数据的对象)的直接访问。我们可以轻松实现访问数据的函数:

std::wstring UserName( ) const
{
    return userName_->AsString( );
}
Int64 UserId( ) const
{
    return userId_->AsInt64( );
}
DateTime Created( ) const
{
    return created_->AsDateTime( );
}

现在,我们可以像这样查询 ALL_USERS 视图:

auto statement = serviceContext.CreateStatement( AllUsersReader::SQL );
auto reader = statement.ExecuteReader<AllUsersReader>( );
while ( reader->Read( ) )
{
    auto userName = reader->UserName( );
    auto userId = reader->UserId( );
    auto created = reader->Created( );
}

虽然工作量大得多,但它的执行效率更高——也许更重要的是:它将查询的内部实现细节与代码的其他部分隔离开来。

在许多情况下,您知道使用 64 位整数比 Oracle Number 更有效,或者 OCI::DateTimestamp 更合适,或者使用 long var binary (LVB) 而不是 BLOB。在许多实际应用中,能够控制数据如何与 Oracle 交换对于解决方案的性能至关重要。

提高性能

本文开头承诺了高性能,而性能是相对的,因此需要一个基准。以下是用于测试用例的表:

CREATE TABLE TimeseriesValue1
(
Id NUMBER(19) NOT NULL,
Ts TIMESTAMP(9) NOT NULL,
Flags NUMBER(19) NOT NULL,
Val BINARY_DOUBLE NOT NULL,
CONSTRAINT PK_TSV1 PRIMARY KEY(Id,Ts)
) ORGANIZATION INDEX;

CREATE TABLE TimeseriesValue2
(
Id NUMBER(19) NOT NULL,
Ts NUMBER(19) NOT NULL,
Flags NUMBER(19) NOT NULL,
Val BINARY_DOUBLE NOT NULL,
CONSTRAINT PK_TSV2 PRIMARY KEY(Id,Ts)
) ORGANIZATION INDEX;

它们几乎相同,只是 TimeseriesValue1Ts 列类型是 TIMESTAMP(9),而 TimeseriesValue2Ts 列类型是 NUMBER(19)NUMBER(19) 的大小足以容纳 64 位整数可以容纳的任何值。

使用 ODP.Net 进行插入

基准测试使用 .NET 5.0 和 Oracle ODP.Net Core 版本 2.19.100,它在一个循环中插入 1,000,000 行数据。

public void BasicInsert()
{
    int count = 1000000;

    var lastTimestamp = new DateTime(2020, 1, 1);
    var firstTimestamp = lastTimestamp - TimeSpan.FromSeconds(count);
    var oneSecond = TimeSpan.FromSeconds(1);

    var stopwatch = new Stopwatch();
    stopwatch.Start();

    for (int i = 0; i < count; ++i)
    {
        var transaction = _connection.BeginTransaction();
        using (transaction)
        {
            using (var command = _connection.CreateCommand())
            {
                command.CommandText = "INSERT INTO TimeseriesValue1(Id,Ts,Flags,Val) " +
                                "VALUES(:1,:2,:3,:4)";

                OracleParameter id = new OracleParameter();
                id.OracleDbType = OracleDbType.Int64;
                id.Value = i + 1;

                OracleParameter timestamp = new OracleParameter();
                timestamp.OracleDbType = OracleDbType.TimeStamp;
                timestamp.Value = firstTimestamp + (oneSecond * (i + 1));

                OracleParameter flag = new OracleParameter();
                flag.OracleDbType = OracleDbType.Int64;
                flag.Value = i + 1;

                OracleParameter value = new OracleParameter();
                value.OracleDbType = OracleDbType.BinaryDouble;
                value.Value = (double)i + 1.0;

                command.Parameters.Add(id);
                command.Parameters.Add(timestamp);
                command.Parameters.Add(flag);
                command.Parameters.Add(value);

                command.ExecuteNonQuery();
            }
            transaction.Commit();
        }
    }
    stopwatch.Stop();
    var duration = stopwatch.Elapsed.TotalSeconds;
    var rowsPerSecond = count / duration;
    System.Console.Out.WriteLine("Inserted {0} rows in {1} seconds - rows per second: {2} ",
        count, duration, rowsPerSecond);
}

输出

Inserted 1000000 rows in 285.0611257 seconds - rows per second: 3508.019543332632

TimeseriesValue2 表执行相同操作可将性能提高约 20%。

Inserted 1000000 rows in 237.306739 seconds - rows per second: 4213.955339886071

逐行插入是大多数客户端应用程序将数据插入数据库的方式。

ODP.Net 有一个很好的功能,允许我们使用对 ExecuteNonQuery() 的一次调用来传递所有数据。

public void Insert()
{
    int count = 1000000;
    long[] ids = new long[count];
    DateTime[] timestamps = new DateTime[count];
    long[] flags = new long[count];
    double[] values = new double[count];

    var lastTimestamp = new DateTime(2020, 1, 1);
    var firstTimestamp = lastTimestamp - TimeSpan.FromSeconds(count);
    var oneSecond = TimeSpan.FromSeconds(1);

    for (int i = 0; i < count; ++i)
    {
        ids[i] = i + 1;
        timestamps[i] = firstTimestamp + (oneSecond * (i + 1));
        flags[i] = i + 1;
        values[i] = i + 1;
    }

    using (var command = _connection.CreateCommand())
    {
        command.CommandText = "INSERT INTO TimeseriesValue1(Id,Ts,Flags,Val) "
                                         +"VALUES(:1,:2,:3,:4)";

        OracleParameter id = new OracleParameter();
        id.OracleDbType = OracleDbType.Int64;
        id.Value = ids;

        OracleParameter timestamp = new OracleParameter();
        timestamp.OracleDbType = OracleDbType.TimeStamp;
        timestamp.Value = timestamps;

        OracleParameter flag = new OracleParameter();
        flag.OracleDbType = OracleDbType.Int64;
        flag.Value = flags;

        OracleParameter value = new OracleParameter();
        value.OracleDbType = OracleDbType.BinaryDouble;
        value.Value = values;

        command.ArrayBindCount = ids.Length;
        command.Parameters.Add(id);
        command.Parameters.Add(timestamp);
        command.Parameters.Add(flag);
        command.Parameters.Add(value);

        var stopwatch = new Stopwatch();
        stopwatch.Start();
        var transaction = _connection.BeginTransaction();
        using (transaction)
        {
            command.ExecuteNonQuery();
            transaction.Commit();
        }
        stopwatch.Stop();
        var duration = stopwatch.Elapsed.TotalSeconds;
        var rowsPerSecond = count / duration;
        System.Console.Out.WriteLine("Inserted {0} rows in {1} seconds - rows per second: {2} ",
            count, duration, rowsPerSecond);
    }
}

输出

Inserted 1000000 rows in 5.1137679 seconds - rows per second: 195550.5254745723

TimeseriesValue2 表执行相同操作可将性能提高约 9%。

Inserted 1000000 rows in 4.6812986 seconds - rows per second: 213615.9398163578

此代码将输入变量绑定到四个数组,每个数组包含 1,000,000 个值,并在 4.68 秒内插入 1,000,000 行。程序每秒插入超过 195,000 行,性能提高了 55 倍以上。

注意:使用 ODP.Net,我能够使用一次调用插入 1,000,000 行,但尝试插入 1,050,000 行会导致 ExecuteNonQuery() 引发异常。

Ts 列的类型更改为 NUMBER(19) 也可以提高性能,而且大多数现代编程环境都使用 64 位整数表示时间——因此,当性能是优先事项时,这绝对是需要考虑的。

使用 Harlinn.OCI 进行插入

让我们使用 Harlinn.OCI 做同样的事情。

std::wstring sql2( L"INSERT INTO TimeseriesValue1(Id,Ts,Flags,Val) "
    L"VALUES( :1, :2, :3, :4)" );

DateTime lastTimestamp( 2020, 1, 1 );
auto firstTimestamp = lastTimestamp - TimeSpan::FromSeconds( count );
auto oneSecond = TimeSpan::FromSeconds( 1 );

Stopwatch stopwatch;
stopwatch.Start( );
for ( size_t i = 0; i < count; ++i )
{
    auto id = i + 1;
    auto timestamp = firstTimestamp + ( oneSecond * ( i + 1 ) );
    auto flags = i + 1;
    auto value = static_cast<double>( i + 1 );

    // Create the statement
    auto insertStatement = 
         serviceContext.CreateStatement( sql2, id, timestamp, flags, value );
    // Execute the insert
    insertStatement.Execute( );
}

// commit the changes
serviceContext.TransactionCommit( );
stopwatch.Stop( );
auto duration = stopwatch.TotalSeconds( );
auto rowsPerSecond = count / duration;
printf( "Inserted %zu rows in %f seconds - %f rows per seconds\n",
    count, duration, rowsPerSecond );

输出

Inserted 1000000 rows in 113.662342 seconds - 8797.988711 rows per seconds

这段代码执行的任务与上面的 ODP.Net 基准测试几乎相同,通过一个循环逐行插入数据,为每次迭代分配一个 OCI::Statement 并绑定输入变量。它的性能比 ODP.Net 版本高出 2.5 倍以上。

切换到 TimeseriesValue2 所实现的性能提升非常小,以至于难以证明其有效性。

Inserted 1000000 rows in 110.978168 seconds - 9010.781308 rows per seconds

使用 ODP.Net 表明,通过绑定到数组并通过一次对 ExecuteNonQuery() 的调用来执行所有插入操作,我们可以显著提高性能,因此,看看这项技术将为 OCI 解决方案带来多大的改进是很有趣的。

// Number of rows to insert
constexpr size_t count = 1'000'000;

std::wstring sql2( L"INSERT INTO TimeseriesValue1(Id,Ts,Flags,Val) "
    L"VALUES( :1, :2, :3, :4)" );

// Create the statement
auto insertStatement = serviceContext.CreateStatement( sql2 );
// Create the bind objects
auto id = insertStatement.Bind<UInt64ArrayBind>( 1 );
auto timestamp = insertStatement.Bind<TimestampArrayBind>( 2 );
auto flag = insertStatement.Bind<UInt64ArrayBind>( 3 );
auto value = insertStatement.Bind<DoubleArrayBind>( 4 );

DateTime lastTimestamp( 2020, 1, 1 );
auto firstTimestamp = lastTimestamp - TimeSpan::FromSeconds( count );
auto oneSecond = TimeSpan::FromSeconds( 1 );

// vectors that will be bound by the bind objects
std::vector<UInt64> ids( count );
std::vector<DateTime> timestamps( count );
std::vector<UInt64> flags( count );
std::vector<double> values( count );
// Initialize the vectors with dummy data
for ( size_t i = 0; i < count; ++i )
{
    ids[i] = i + 1;
    timestamps[i] = firstTimestamp + ( oneSecond * ( i + 1 ) );
    flags[i] = i + 1;
    values[i] = static_cast<double>( i + 1 );
}
// Assign the vectors to the bind objects
id->Assign( std::move( ids ) );
timestamp->Assign( timestamps );
flag->Assign( std::move( flags ) );
value->Assign( std::move( values ) );

// Execute the inserts and commit the changes
Stopwatch stopwatch;
stopwatch.Start( );
insertStatement.Execute( count );
serviceContext.TransactionCommit( );
stopwatch.Stop( );
auto duration = stopwatch.TotalSeconds( );
auto rowsPerSecond = count / duration;
printf( "Inserted %zu rows in %f seconds - %f rows per seconds\n",
    count, duration, rowsPerSecond );

输出

Inserted 1000000 rows in 0.814117 seconds - 1228324.072179 rows per seconds

此版本与最快的 .NET 版本相比,性能提高了 5.75 倍——这表明 Oracle Call Interface 的效率可以远高于 ODP.Net。与基本的 ODP.Net 版本相比,我们将性能提高了 350 倍,因此这绝对值得付出额外的努力;而坦白说,这些努力并不算大。

切换到 TimeseriesValue2 后,代码的性能略差于 TimeseriesValue1,但同样,差异非常小,无法证明其有效性。

Inserted 1000000 rows in 0.818555 seconds - 1221665.604225 rows per seconds

OCI 处理大量数据的能力也更强,此版本可以轻松地使用单个调用插入 100,000,000 行,但性能会降低。

Inserted 100000000 rows in 106.102264 seconds - 942486.955012 rows per seconds

使用 ODP.Net 进行选择

再次使用 .NET 5.0 和 ODP.Net 来建立基准。

public void Select()
{
    using (var command = _connection.CreateCommand())
    {
        int count = 0;
        var stopwatch = new Stopwatch();
        command.CommandText = "SELECT Id,Ts,Flags,Val FROM TimeseriesValue2 ORDER BY Id,Ts";

        stopwatch.Start();
        var reader = command.ExecuteReader();
        while (reader.Read())
        {
            var id = reader.GetInt64(0);
            var ts = reader.GetInt64(1);
            var flags = reader.GetInt64(2);
            var value = reader.GetDouble(3);
            count++;
        }
        stopwatch.Stop();
        var duration = stopwatch.Elapsed.TotalSeconds;
        var rowsPerSecond = count / duration;
        System.Console.Out.WriteLine("Retrieved {0} rows in {1} seconds - "+
                                     "rows per second: {2} ",
            count, duration, rowsPerSecond);
    }
}

输出

Retrieved 1000000 rows in 0.5656143 seconds - rows per second: 1767989.2463822078

这出奇地好。:-)

使用 Harlinn.OCI 进行选择

最初的 C++ 实现性能仅略有提升。

auto statement = serviceContext.CreateStatement( L"SELECT Id,Ts,Flags,Val " 
                                                 L"FROM TimeseriesValue2" );
statement.SetPrefetchRows( 30'000 );

Stopwatch stopwatch;
stopwatch.Start( );
auto reader = statement.ExecuteReader<ArrayDataReader>( 120'000 );

size_t count = 0;
while ( reader->Read( ) )
{
    auto id = reader->GetUInt64( 0 );
    auto ts = reader->GetUInt64( 1 );
    auto flags = reader->GetUInt64( 2 );
    auto value = reader->GetDouble( 3 );
    count++;
}
stopwatch.Stop( );
auto duration = stopwatch.TotalSeconds( );
auto rowsPerSecond = count / duration;
printf( "Retrieved %zu rows in %f seconds - %f rows per seconds\n",
    count, duration, rowsPerSecond );

输出

Retrieved 1000000 rows in 0.413025 seconds - 2421160.946674 rows per seconds

在这里,我们使用 C++ 和 OCI 的吞吐量提高了约 36%,但我们应该能做得更好。是时候使用自定义数据读取器了。

class TimeseriesValues2Reader : public ArrayDataReader
{
public:
    using Base = ArrayDataReader;
    constexpr static UInt32 ID_ID = 0;
    constexpr static UInt32 TS_ID = 1;
    constexpr static UInt32 FLAGS_ID = 2;
    constexpr static UInt32 VAL_ID = 3;

    constexpr static wchar_t SQL[] = L"SELECT Id,Ts,Flags,Val FROM TimeseriesValue2";
public:
    TimeseriesValues2Reader( const OCI::Statement& statement, size_t size )
        : Base( statement, size )
    {
    }
    virtual void InitializeDefines( ) override
    {
        Define<UInt64>( ID_ID + 1 );
        Define<UInt64>( TS_ID + 1 );
        Define<UInt64>( FLAGS_ID + 1 );
        Define<Double>( VAL_ID + 1 );
    }
    UInt64 Id( ) const { return GetUInt64( ID_ID ); }
    UInt64 Timestamp( ) const { return GetUInt64( TS_ID ); }
    UInt64 Flag( ) const { return GetUInt64( FLAGS_ID ); }
    Double Value( ) const { return GetDouble( VAL_ID ); }
};

InitializeDefines( ) 的实现使我们能够控制 select-list 中每个元素的本机数据格式。这带来了巨大的改进。

constexpr UInt32 PrefetchRows = 32'000;
auto statement = serviceContext.CreateStatement( TimeseriesValues2Reader::SQL );
statement.SetPrefetchRows( PrefetchRows );

Stopwatch stopwatch;
stopwatch.Start( );
auto reader = statement.ExecuteReader<TimeseriesValues2Reader>( PrefetchRows*5 );
size_t count = 0;
while ( reader->Read( ) )
{
    auto id = reader->Id( );
    auto ts = reader->Timestamp( );
    auto flags = reader->Flag( );
    auto value = reader->Value( );
    count++;
}

stopwatch.Stop( );
auto duration = stopwatch.TotalSeconds( );
auto rowsPerSecond = count / duration;
printf( "Retrieved %zu rows in %f seconds - %f rows per seconds\n",
    count, duration, rowsPerSecond );

输出

Retrieved 1000000 rows in 0.294642 seconds - 3393944.659696 rows per seconds

C++ 实现比 ODP.Net 和 .Net 5.0 快 91%,这包括解析、执行和从服务器上的游标获取数据所花费的时间——在这种情况下,根据 V$SQLAREA 视图的 ELAPSED_TIME 列,大约为 100 毫秒;以及通过 TCP/IP 堆栈传递数据所花费的时间。

我们能做得更好吗?Harlinn.OCI 尚未支持绑定到结构,但这需要进一步探索。

首先,我们需要一个与数据 select-list 匹配的结构:

struct TimeseriesValue
{
    Int64 Id;
    Int64 Timestamp;
    Int64 Flags;
    Double Value;
};

我们仍然使用 Harlinn.OCI 来创建语句,并配置预取行数。

constexpr UInt32 PrefetchRows = 32'000;

auto statement = serviceContext.CreateStatement( L"SELECT Id,Ts,Flags,Val "
                                                 L"FROM TimeseriesValue2" );
statement.SetPrefetchRows( PrefetchRows );

解决完这些问题后,我们需要分配将接收 OCI 数据的内存。

std::vector<TimeseriesValue> values( PrefetchRows*4 );
TimeseriesValue* data = values.data( );

OCIStmt* statementHandle = (OCIStmt*)statement.Handle();
auto& error = statement.Error( );
OCIError* errorHandle = (OCIError*)error.Handle( );

当我们希望 OCI 将数据直接放入结构时,我们像往常一样调用 OCIDefineByPos2,将向量第一个元素的地址、大小和类型传递过去。

OCIDefine* idDefineHandle = nullptr;
auto rc = OCIDefineByPos2( statementHandle, &idDefineHandle, errorHandle, 1,
    &data->Id, sizeof( data->Id ), SQLT_INT,
    nullptr, nullptr, nullptr, OCI_DEFAULT );
if ( rc < OCI_SUCCESS )
{
    error.CheckResult( rc );
}

然后,我们需要让 OCI 知道向量下一个元素与该变量之间的距离。

rc = OCIDefineArrayOfStruct( idDefineHandle, errorHandle,
    sizeof( TimeseriesValue ), 0,  0, 0 );
if ( rc < OCI_SUCCESS )
{
    error.CheckResult( rc );
}

然后,对结构体的其余成员重复此过程。

OCIDefine* timestampDefineHandle = nullptr;
rc = OCIDefineByPos2( statementHandle, &timestampDefineHandle, errorHandle, 2,
    &data->Timestamp, sizeof( data->Timestamp ), SQLT_INT,
    nullptr, nullptr, nullptr, OCI_DEFAULT );
if ( rc < OCI_SUCCESS )
{
    error.CheckResult( rc );
}
rc = OCIDefineArrayOfStruct( timestampDefineHandle, errorHandle,
    sizeof( TimeseriesValue ), 0, 0, 0 );
if ( rc < OCI_SUCCESS )
{
    error.CheckResult( rc );
}
OCIDefine* flagsDefineHandle = nullptr;
rc = OCIDefineByPos2( statementHandle, &flagsDefineHandle, errorHandle, 3,
    &data->Flags, sizeof( data->Timestamp ), SQLT_INT,
    nullptr, nullptr, nullptr, OCI_DEFAULT );
if ( rc < OCI_SUCCESS )
{
    error.CheckResult( rc );
}
rc = OCIDefineArrayOfStruct( flagsDefineHandle, errorHandle,
    sizeof( TimeseriesValue ), 0, 0, 0 );
if ( rc < OCI_SUCCESS )
{
    error.CheckResult( rc );
}

OCIDefine* valueDefineHandle = nullptr;
rc = OCIDefineByPos2( statementHandle, &valueDefineHandle, errorHandle, 4,
    &data->Value, sizeof( data->Value ), SQLT_BDOUBLE,
    nullptr, nullptr, nullptr, OCI_DEFAULT );
if ( rc < OCI_SUCCESS )
{
    error.CheckResult( rc );
}
rc = OCIDefineArrayOfStruct( valueDefineHandle, errorHandle,
    sizeof( TimeseriesValue ), 0, 0, 0 );
if ( rc < OCI_SUCCESS )
{
    error.CheckResult( rc );

}

现在我们已经为查询的每个列创建了定义,是时候在 Oracle 数据库上执行查询了。

OCISvcCtx* serviceContextHandle = (OCISvcCtx*)serviceContext.Handle( );
Stopwatch stopwatch;
stopwatch.Start( );
size_t count = 0;
rc = OCIStmtExecute( serviceContextHandle, statementHandle, errorHandle,
                    0, 0, NULL, NULL, OCI_DEFAULT );
if ( rc < OCI_SUCCESS )
{
    error.CheckResult( rc );
}

只有在从 OCI 接收数据时,我们才需要指定向量的大小。

rc = OCIStmtFetch2( statementHandle, errorHandle,
                static_cast<UInt32>( values.size( ) ),
                OCI_FETCH_NEXT, 0, OCI_DEFAULT );

此时,数据已放入向量中,通过调用 RowsFetched( ) 可以确定提取了多少行数据。

if ( rc >= OCI_SUCCESS )
{
    UInt32 rowsFetched = statement.RowsFetched( );
    while ( rowsFetched && rc >= OCI_SUCCESS )
    {
        for ( UInt32 i = 0; i < rowsFetched; ++i )
        {
            auto id = data[i].Id;
            auto ts = data[i].Timestamp;
            auto flags = data[i].Flags;
            auto value = data[i].Value;
            count++;
        }
        if ( rc != OCI_NO_DATA )
        {
            rc = OCIStmtFetch2( statementHandle, errorHandle,
                        static_cast<UInt32>( values.size( ) ),
                        OCI_FETCH_NEXT, 0, OCI_DEFAULT );
            rowsFetched = statement.RowsFetched( );
        }
        else
        {
            rowsFetched = 0;
        }
    }
}
if ( rc < OCI_SUCCESS )
{
    error.CheckResult( rc );
}

stopwatch.Stop( );
auto duration = stopwatch.TotalSeconds( );
auto rowsPerSecond = count / duration;
printf( "Retrieved %zu rows in %f seconds - %f rows per seconds\n",
    count, duration, rowsPerSecond );

输出

Retrieved 1000000 rows in 0.276765 seconds - 3613176.242065 rows per seconds

这比自定义数据读取器将性能提高了约 6%。

使用 ODP.Net 进行更新

更新数据库中最常见的方法是为每一行执行一个 update 语句,如下所示:

public void BasicUpdate()
{
    var rows = GetAll();
    var stopwatch = new Stopwatch();
    stopwatch.Start();
    foreach (var row in rows)
    {
        using (var command = _connection.CreateCommand())
        {
            command.CommandText = "UPDATE TimeseriesValue1 SET Flags=:1, Val=:2 "
                                + "WHERE Id=:3 AND Ts=:4";

            OracleParameter flag = new OracleParameter();
            flag.OracleDbType = OracleDbType.Int64;
            flag.Value = row.Flags * 2;

            OracleParameter value = new OracleParameter();
            value.OracleDbType = OracleDbType.BinaryDouble;
            value.Value = row.Value * 2;

            OracleParameter id = new OracleParameter();
            id.OracleDbType = OracleDbType.Int64;
            id.Value = row.Id;

            OracleParameter timestamp = new OracleParameter();
            timestamp.OracleDbType = OracleDbType.TimeStamp;
            timestamp.Value = row.Timestamp;

            command.Parameters.Add(flag);
            command.Parameters.Add(value);
            command.Parameters.Add(id);
            command.Parameters.Add(timestamp);

            command.ExecuteNonQuery();
        }
    }
    stopwatch.Stop();
    var duration = stopwatch.Elapsed.TotalSeconds;
    var rowsPerSecond = rows.Count / duration;
    System.Console.Out.WriteLine("Updated {0} rows in {1} seconds - rows per second: {2} ",
        rows.Count, duration, rowsPerSecond);
}

输出

Updated 1000000 rows in 199.6225356 seconds - rows per second: 5009.454453598274

正如我们在 insert 操作中看到的,通过绑定到数组并仅通过一次 ExecuteNonQuery() 调用来执行所有更新,可以大大提高性能。

public void Update()
{
    var rows = GetAll();
    long[] ids = new long[rows.Count];
    DateTime[] timestamps = new DateTime[rows.Count];
    long[] flags = new long[rows.Count];
    double[] values = new double[rows.Count];

    for (int i = 0; i < rows.Count; ++i)
    {
        var row = rows[i];
        ids[i] = row.Id;
        timestamps[i] = row.Timestamp;
        flags[i] = row.Flags * 2;
        values[i] = row.Value* 2;
    }

    using (var command = _connection.CreateCommand())
    {
        command.CommandText = "UPDATE TimeseriesValue1 SET Flags=:1, Val=:2 "
                            + "WHERE Id=:3 AND Ts=:4";

        OracleParameter flag = new OracleParameter();
        flag.OracleDbType = OracleDbType.Int64;
        flag.Value = flags;

        OracleParameter value = new OracleParameter();
        value.OracleDbType = OracleDbType.BinaryDouble;
        value.Value = values;

        OracleParameter id = new OracleParameter();
        id.OracleDbType = OracleDbType.Int64;
        id.Value = ids;

        OracleParameter timestamp = new OracleParameter();
        timestamp.OracleDbType = OracleDbType.TimeStamp;
        timestamp.Value = timestamps;

        command.ArrayBindCount = rows.Count;
        command.Parameters.Add(flag);
        command.Parameters.Add(value);
        command.Parameters.Add(id);
        command.Parameters.Add(timestamp);

        int count = 0;
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        var transaction = _connection.BeginTransaction();
        using (transaction)
        {
            count = command.ExecuteNonQuery();
            transaction.Commit();
        }
        stopwatch.Stop();
        var duration = stopwatch.Elapsed.TotalSeconds;
        var rowsPerSecond = count / duration;
        System.Console.Out.WriteLine("Updated {0} rows in {1} seconds - rows per second: {2} ",
            count, duration, rowsPerSecond);
    }
}

输出

Updated 1000000 rows in 11,9057144 seconds - rows per second: 83993,279731286

这可以将性能提高 16 倍——这绝对是值得的改进。

使用 Harlinn.OCI 进行更新

使用 OCI 实现基本的 update 循环:

Stopwatch stopwatch;
stopwatch.Start( );
constexpr wchar_t sql[] = 
    L"UPDATE TimeseriesValue1 SET Flags=:1, Val=:2 WHERE Id=:3 AND Ts=:4";

for ( auto& row : rows )
{
    auto updateStatement = serviceContext.CreateStatement( sql, 
                           row.Flags*2, row.Value*2, row.Id, row.Timestamp );
    updateStatement.ExecuteNonQuery( );
}

stopwatch.Stop( );
auto duration = stopwatch.TotalSeconds( );
auto rowsPerSecond = count / duration;
printf( "Updated %zu rows in %f seconds - %f rows per seconds\n",
    count, duration, rowsPerSecond );

输出

Updated 1000000 rows in 123.436877 seconds - 8101.306717 rows per seconds

与使用 ODP.Net 实现的基本更新循环相比,性能提高了 61%;当我们使用 OCI 进行数组绑定时:

constexpr wchar_t sql[] = 
     L"UPDATE TimeseriesValue1 SET Flags=:1, Val=:2 WHERE Id=:3 AND Ts=:4";
auto updateStatement = serviceContext.CreateStatement( sql );

auto flag = updateStatement.Bind<UInt64ArrayBind>( 1 );
auto value = updateStatement.Bind<DoubleArrayBind>( 2 );
auto id = updateStatement.Bind<UInt64ArrayBind>( 3 );
auto timestamp = updateStatement.Bind<TimestampArrayBind>( 4 );

std::vector<UInt64> ids( count );
std::vector<DateTime> timestamps( count );
std::vector<UInt64> flags( count );
std::vector<double> values( count );

for ( size_t i = 0; i < count; ++i )
{
    auto& row = rows[i];
    ids[i] = row.Id;
    timestamps[i] = row.Timestamp;
    flags[i] = row.Flags*2;
    values[i] = row.Value*2;
}

// Assign the vectors to the bind objects
id->Assign( std::move( ids ) );
timestamp->Assign( timestamps );
flag->Assign( std::move( flags ) );
value->Assign( std::move( values ) );

Stopwatch stopwatch;
stopwatch.Start( );

updateStatement.ExecuteNonQuery( static_cast<UInt32>( count ) );
serviceContext.TransactionCommit( );

stopwatch.Stop( );

auto duration = stopwatch.TotalSeconds( );
auto rowsPerSecond = count / duration;
printf( "Updated %zu rows in %f seconds - %f rows per seconds\n",
    count, duration, rowsPerSecond );

输出

Updated 1000000 rows in 6.182285 seconds - 161752.501656 rows per seconds

与基本的 ODP.Net 更新循环相比,性能提高了 32 倍,并且比 ODP.Net 的数组绑定快 两倍

使用单个 ExecuteNonQuery 调用更新 100,000,000 行是可能的,但这会显著降低性能。

Updated 100000000 rows in 1131.646176 seconds - 88366.842991 rows per seconds

结束(暂时……)

有趣的是,Oracle 插入数据的速度远快于更新行。

大多数事务性数据库管理系统都利用一种称为多版本并发控制的技术。因此,更新操作永远不会更新现有记录,而是创建一个副本,该副本只有在更新提交到数据库后才能被其他会话可见。一些系统提供多种事务隔离级别,有时允许并行会话读取尚未由执行 updateinsert 的会话提交的数据。这些系统仍然会创建更新行的副本,否则它们将无法回滚当前事务。以一致的方式管理数据的多个版本很复杂,因此它对服务器的整体性能产生重大影响也就不足为奇了。

由于 insert 操作通常比 update 操作性能更好;尤其是对于列少且固定大小的表;这一点可以在设计数据库时加以考虑。从长远来看,这可能会使数据库更有价值,因为它现在可以分析更改数据的影响,可能使用机器学习或更传统的统计方法。

历史

  • 2020年12月3日 - 初始帖子
  • 2020年12月18日:
    • IO::FileStream 的错误修复
    • 支持初始 HTTP 服务器开发
      • 同步服务器:$(SolutionDir)Examples\Core\HTTP\Server\HttpServerEx01
      • 异步服务器:$(SolutionDir)Examples\Core\HTTP\Server\HttpServerEx02
    • 使用 Windows 线程池 API 简化了异步 I/O、计时器、工作和 Windows 可等待内核对象的事件:$(SolutionDir)Examples\Core\ThreadPools\HExTpEx01
  • 2021年2月11日
    • Bug 修复
    • 初始 C++ ODBC 支持
  • 2021年2月25日
    • 更新 LMDB
    • 更新 xxHash
    • 添加了使用 LMDB 对大型复杂键的超快速基于哈希的索引的初始实现
    • 快速异步日志记录 - 接近完成 :-)
  • 2021年3月3日
    • 新的授权相关类
      • SecurityId:SID 和相关操作的包装器
      • ExplicitAccess:EXPLICIT_ACCESS 的包装器
      • Trustee:TRUSTEE 的包装器
      • SecurityIdAndDomain:保存 LookupAccountName 的结果
      • LocalUniqueId:LUID 的包装器
      • AccessMask:方便检查分配给 ACCESS_MASK 的权限。
        • AccessMaskT<>
          • EventWaitHandleAccessMask:检查和操作 EventWaitHandle 的权限。
          • MutexAccessMask:检查和操作 Mutex 的权限。
          • SemaphoreAccessMask:检查和操作 Semaphore 的权限。
          • WaitableTimerAccessMask:检查和操作 WaitableTimer 的权限。
          • FileAccessMask:检查和操作文件相关权限。
          • DirectoryAccessMask:检查和操作目录相关权限。
          • PipeAccessMask:检查和操作管道相关权限。
          • ThreadAccessMask:检查和操作线程相关权限。
          • ProcessAccessMask:检查和操作进程相关权限。
      • GenericMapping:GENERIC_MAPPING 的包装器
      • AccessControlEntry:这是一组小类,包装了 ACE 结构。
        • AccessControlEntryBase<,>
          • AccessAllowedAccessControlEntry
          • AccessDeniedAccessControlEntry
          • SystemAuditAccessControlEntry
          • SystemAlarmAccessControlEntry
          • SystemResourceAttributeAccessControlEntry
          • SystemScopedPolicyIdAccessControlEntry
          • SystemMandatoryLabelAccessControlEntry
          • SystemProcessTrustLabelAccessControlEntry
          • SystemAccessFilterAccessControlEntry
          • AccessDeniedCallbackAccessControlEntry
          • SystemAuditCallbackAccessControlEntry
          • SystemAlarmCallbackAccessControlEntry
        • ObjectAccessControlEntryBase<,>
          • AccessAllowedObjectAccessControlEntry
          • AccessDeniedObjectAccessControlEntry
          • SystemAuditObjectAccessControlEntry
          • SystemAlarmObjectAccessControlEntry
          • AccessAllowedCallbackObjectAccessControlEntry
          • AccessDeniedCallbackObjectAccessControlEntry
          • SystemAuditCallbackObjectAccessControlEntry
          • SystemAlarmCallbackObjectAccessControlEntry
      • AccessControlList:ACL 的包装器。
      • PrivilegeSet:PRIVILEGE_SET 的包装器。
      • SecurityDescriptor:包装 SECURITY_DESCRIPTOR 的早期实现。
      • SecurityAttributes:包装 SECURITY_ATTRIBUTES 的非常早期的实现。
      • Token:访问令牌的包装器的早期实现。
      • DomainObject
        • User:有关本地、工作组或域用户的信息。
        • Computer:有关本地、工作组或域计算机的信息。
        • Group:本地、工作组或域组。
      • Users:User 对象向量。
      • Groups:Group 对象向量。
  • 2021年3月14日 - 更多关于安全方面的工作:
    • Token:Windows 访问令牌的包装器,带有多个支持类,如:
      • TokenAccessMask:Windows 访问令牌访问权限的访问掩码实现。
      • TokenGroups:Windows TOKEN_GROUPS 类型的包装器/二进制兼容替代品,具有 C++ 容器风格的接口。
      • TokenPrivileges:Windows TOKEN_PRIVILEGES 类型的包装器/二进制兼容替代品,具有 C++ 容器风格的接口。
      • TokenStatistics:Windows TOKEN_STATISTICS 类型的二进制兼容替代品,使用库实现的类型,如 LocalUniqueId、TokenType 和 ImpersonationLevel。
      • TokenGroupsAndPrivileges:Windows TOKEN_GROUPS_AND_PRIVILEGES 类型的包装器/二进制兼容替代品。
      • TokenAccessInformation:Windows TOKEN_ACCESS_INFORMATION 类型的包装器/二进制兼容替代品。
      • TokenMandatoryLabel:Windows TOKEN_MANDATORY_LABEL 类型的包装器。
    • SecurityPackage:提供对 Windows 安全包信息的访问。
    • SecurityPackages:有关系统上安装的安全包信息的 std::unordered_map。
    • CredentialsHandle:Windows CredHandle 类型的包装器。
    • SecurityContext:Windows CtxtHandle 类型的包装器。
    • Crypto::Blob 和 Crypto::BlobT:C++ 风格的 _CRYPTOAPI_BLOB 替代品。
    • CertificateContext:Windows PCCERT_CONTEXT 类型的包装器,提供对 X.509 证书的访问。
    • CertificateChain:Windows PCCERT_CHAIN_CONTEXT 类型的包装器,其中包含简单的证书链数组和一个信任状态结构,该结构指示所有连接的简单链的摘要有效性数据。
    • ServerOcspResponseContext:包含已编码的 OCSP 响应。
    • ServerOcspResponse:表示与服务器证书链关联的 OCSP 响应的句柄。
    • CertificateChainEngine:表示应用程序的链引擎。
    • CertificateTrustList:Windows PCCTL_CONTEXT 类型的包装器,其中包含 CTL 的已编码和已解码表示。它还包含一个打开的 HCRYPTMSG 句柄,用于已解码的、加密签名的消息,该消息包含 CTL_INFO 作为其内部内容。
    • CertificateRevocationList:包含证书吊销列表 (CRL) 的已编码和已解码表示。
    • CertificateStore:用于存储证书、证书吊销列表 (CRL) 和证书信任列表 (CTL) 的存储。
  • 2021年3月23日
    • 更新至 Visual Studio 16.9.2
    • 构建修复
    • SecurityDescriptor: 实现了安全描述符的序列化,从而能够持久化授权数据。
© . All rights reserved.