SAP Connector for Microsoft .NETでSAPから大量データを効率的に取得する

SAP Connector for Microsoft .NET は .NET アプリケーションから SAP の BAPI 呼び出し等を実行できる SAP 社公式のライブラリです。

本記事では SAP Connector for Microsoft .NET を使用して BAPI を呼び出し、取得した大量データを SQL Server に連携する方法について紹介します。


はじめに

SAP Connector for Microsoft .NET (以下 NCo )は .NET Framework のアプリから SAP の BAPI を呼び出してデータの入出力を行うことができるライブラリです。最新バージョンは 3.1 で、 .NET Framework 4.6.2 から 4.8.x までで使用することができます( .NET 6.0 / 8.0 では動作しません)。

弊社は ERP システムとして SAP S4/HANA を導入しており1、社内アプリ、レポート類の作成には SAP 内に蓄積されている各種データの活用が必須です。またそのためには SAP から社内の SAP 外環境に対してデータをエクスポートすることも必要となります。

本記事では NCo を使用して BAPI を呼び出して大量のデータを取得し、取得したデータを効率的に SQL Server に対して BULK INSERT する方法を紹介します。

なお筆者は SAP に全く詳しくないことを予めお断りさせていただきます。

環境情報

要素 バージョン
OS Windows 11 22H2
Visual Studio 2022 (17.9.6)
.NET SDK 8.0.204
.NET Framework 4.8.1
NCo 3.1.4.0

本記事に記載のコードは以下のレポジトリに保管してあります。

github.com

NCo を使用して大量データを取得する

はじめに NCo を使用して BAPI を呼び出し、データを取得するところから始めましょう。手順としては次の3段階です。

  1. NCo インストール
  2. .NET プロジェクト準備
  3. BAPI 呼び出し実装

なお NCo の使用方法の詳細は、 SAP 社から公開されているプログラミングガイドやサンプルコードもぜひご参照ください。

NCo インストール

まずはじめに、以下のサイトから NCo のインストーラーを SAP 社のサイトより取得し、インストールを実施します。

SAP Connector for Microsoft .NET

インストーラーは 32bit 用と 64bit 用とで別になっていますので、バージョン 3.1 の 64bit 用を取得します。取得には SAP Universal ID が必要です。

.NET プロジェクト準備

続いて .NET プロジェクトを準備します。

近年の .NET 開発には古いプロジェクト方式と新しい SDK スタイルのプロジェクト方式があり、後者の方がプロジェクト構成がシンプルなため、今回は後者を選択します。

ただし SDK スタイルの .NET Framework プロジェクトは Visual Studio からは直接作成ができないため、一度 .NET 8.0 でプロジェクトを作成した後に、 .NET Framework へと変更を行います。

プロジェクトの作成と変換

Visual Studio から C# の「ワーカーサービス」プロジェクトを新規作成します。作成の際はフレームワークを「.NET 8.0」とし、「最上位レベルのステートメントを使用しない」にチェックを入れて作成します。

プロジェクトが作成できたら、作成されたプロジェクトの csproj ファイルを開き、ターゲットフレームワークを .NET 8.0 から .NET Framework 4.8.1 へ変更します2。またプラットフォームターゲットを x64 に指定します。

<!-- Before -->
<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UserSecretsId>dotnet-SapDataCopyApp-d1ec7e27-6188-4bdf-9b65-422befb2a328</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
  </ItemGroup>
</Project>

<!-- ------------------------------ -->
<!-- After -->
<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net481</TargetFramework>
    <PlatformTarget>x64</PlatformTarget>
    <UserSecretsId>dotnet-SapDataCopyApp-d1ec7e27-6188-4bdf-9b65-422befb2a328</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
  </ItemGroup>
</Project>

ターゲットフレームワークを .NET 8.0 から .NET Framework 4.8.1 に変更すると名前空間のファイルスコープと Global Using でエラーになりますので、 Program.csWorker.cs を開いて名前空間方式の変更と using の追加を行います。

以下は Program.cs の変更例です。

// Before
namespace SapDataCopyApp;

public class Program
{
    public static void Main(string[] args)
    {
        var builder = Host.CreateApplicationBuilder(args);
        builder.Services.AddHostedService<Worker>();

        var host = builder.Build();
        host.Run();
    }
}

// ------------------------------
// After
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace SapDataCopyApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = Host.CreateApplicationBuilder(args);
            builder.Services.AddHostedService<Worker>();

            var host = builder.Build();
            host.Run();
        }
    }
}
NCo 参照の追加

プロジェクトの作成ができたら、続いてプロジェクトに対して NCo アセンブリの参照を追加します。

事前に実施した NCo インストールによって、 NCo のアセンブリは GAC にインストールされていますので、プロジェクトに対して「アセンブリ参照の追加」で以下3つを追加します。

  • sapnco
  • sapnco_utils
  • rscp4n

C:\Windows\Microsoft.NET\assembly\GAC_64 ディレクトリにありますので探してください。アセンブリ参照の追加が成功すると、 csproj に以下項目が追加されています。

<ItemGroup>
  <Reference Include="rscp4n">
    <HintPath>C:\Windows\Microsoft.NET\assembly\GAC_64\rscp4n\v4.0_1.1.0.0__50436dca5c7f7d23\rscp4n.dll</HintPath>
  </Reference>
  <Reference Include="sapnco">
    <HintPath>C:\Windows\Microsoft.NET\assembly\GAC_64\sapnco\v4.0_3.1.0.42__50436dca5c7f7d23\sapnco.dll</HintPath>
  </Reference>
  <Reference Include="sapnco_utils">
    <HintPath>C:\Windows\Microsoft.NET\assembly\GAC_64\sapnco_utils\v4.0_3.1.0.42__50436dca5c7f7d23\sapnco_utils.dll</HintPath>
  </Reference>
</ItemGroup>
設定ファイルの追加

NCo のアセンブリが追加されたら、 App.config ファイルを追加して NCo の設定を記載します。 SAP 接続に使用するユーザーの設定方法は NCo のプログラミングガイドをご参照ください。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <!-- NCoの設定セクション定義の追加 -->
    <sectionGroup name="SAP.Middleware.Connector">
      <section name="GeneralSettings" type="SAP.Middleware.Connector.RfcGeneralConfiguration, sapnco, Version=3.1.0.42, Culture=neutral, PublicKeyToken=50436dca5c7f7d23" />
      <sectionGroup name="ClientSettings">
        <section name="DestinationConfiguration" type="SAP.Middleware.Connector.RfcDestinationConfiguration, sapnco, Version=3.1.0.42, Culture=neutral, PublicKeyToken=50436dca5c7f7d23" />
      </sectionGroup>
    </sectionGroup>
  </configSections>

  <!-- NCoの設定セクション -->
  <SAP.Middleware.Connector>
    <!-- NCoのログ設定 -->
    <GeneralSettings defaultTraceLevel="3" traceDir="Logs\nco-logs" traceEncoding="UTF-8" traceType="PROCESS" />
    <!-- NCoがSAP接続に使うユーザーの設定 -->
    <ClientSettings>
      <DestinationConfiguration>
        <destinations>
          <add NAME="SAMPLE"
               USER="ユーザー名"
               PASSWD="パスワード"
               CLIENT="クライアント番号"
               LANG="言語"
               ASHOST="SAPホスト名"
               SYSNR="システムナンバー"
               SYSID="システムID"
               POOL_SIZE="5"
               MAX_POOL_SIZE="10" />
        </destinations>
      </DestinationConfiguration>
    </ClientSettings>
  </SAP.Middleware.Connector>
</configuration>

BAPI 呼び出し実装

NCo を使用するための .NET プロジェクト準備ができましたので、 SAP に接続して BAPI を呼び出してみましょう。

SAP との接続確認

まずは SAP との接続を確認するために PING を送信してみます。

サービスを停止するために Worker.csIHostApplicationLifetime を追加します。また NCo の RfcDestinationManager から App.config に登録した SAP 接続情報を取得し、 PING コマンドを実行します。

// Worker.cs

using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SAP.Middleware.Connector;

namespace SapDataCopyApp
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;
        private readonly IHostApplicationLifetime _applicationLifetime;

        public Worker(ILogger<Worker> logger, IHostApplicationLifetime applicationLifetime)
        {
            _logger = logger;
            _applicationLifetime = applicationLifetime;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            try
            {
                // "SAMPLE" という名前で登録したSAP接続情報を取得
                RfcDestination destination = RfcDestinationManager.GetDestination("SAMPLE");

                // PINGを送信してSAPに接続できるかを確認
                destination.Ping();
            }
            finally
            {
                // アプリケーションの停止を通知
                _applicationLifetime.StopApplication();
            }
        }
    }
}

この状態でデバッグ実行を行うと、 App.config に記載した SAP の接続情報に誤りが無ければ PING が送信され一瞬でアプリが終了します。

BAPI 呼び出し

SAP との接続が確認できたら実際に BAPI を呼び出してみます。今回は SAP に標準で備わっている BAPI_CURRENCY_GETLIST を実行し、登録されている通貨一覧を取得します。

// Worker.cs

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    try
    {
        // "SAMPLE" という名前で登録したSAP接続情報を取得
        RfcDestination destination = RfcDestinationManager.GetDestination("SAMPLE");

        // PINGを送信してSAPに接続できるかを確認
        destination.Ping();

        // BAPI_CURRENCY_GETLISTの関数を取得
        IRfcFunction function = destination.Repository.CreateFunction("BAPI_CURRENCY_GETLIST");

        // BAPI_CURRENCY_GETLISTを実行
        function.Invoke(destination);

        // BAPI_CURRENCY_GETLISTの結果を取得
        IRfcTable table = function.GetTable("CURRENCY_LIST");
        _logger.LogInformation("取得した通貨の数: {0}", table.RowCount);

        // 最初の1行を取得してログに出力
        IRfcStructure row = table.First();
        foreach (IRfcField col in row)
        {
            _logger.LogInformation("{0}: {1}", col.Metadata.Name, col.GetString());
        }
    }
    finally
    {
        // アプリケーションの停止を通知
        _applicationLifetime.StopApplication();
    }
}

Repository.CreateFunction で実行する BAPI を指定した IRfcFunction を取得します。また Invoke メソッドを呼び出すことで BAPI をリモート実行します。実行結果は Invoke した IRfcFunction に格納されますので、実行後に GetTable メソッドでテーブルオブジェクトを取得して中身を確認します。

ここまでの設定内容に間違いが無ければ、実行することで SAP に登録されている通貨の数、および最初に取得された通貨レコード情報が表示されます。

NCo で取得した大量データを SQL Server に BULK INSERT する

無事 NCo を使用して SAP に接続してリモートから BAPI 呼び出しを行いデータを取得できましたので、続いては取得されたデータを SQL Server に対して BULK INSERT しましょう。

なお、接続先の SQL Server には以下のテーブルを作成しています。

通貨登録先のテーブル情報

こちらの手順は次の3段階となります。

  1. SqlBulkCopy をプロジェクトに追加
  2. SqlBulkCopy で BULK INSERT
  3. より効率的な BULK INSERT

SqlBulkCopy は SQL Server に対して大変効率的にデータ転送することができるクラスです。詳細や使用方法は筆者の別媒体の記事にて解説したことがありますので参考までに記載しておきます。

noxi515.hateblo.jp

SqlBulkCopy をプロジェクトに追加

SqlBulkCopy は SqlClient パッケージの機能の一つとして提供されており、 NuGet からライブラリーを追加することで使用することができます。

今回は Microsoft.Data.SqlClient を追加します。追加すると csproj に以下の行が追加されているはずです。

<ItemGroup>
  <PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0" /> <!-- 追加 -->
  <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
</ItemGroup>

続いて DB 接続文字列を appsettings.json に追加してコードから取得します。

// appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  // 接続文字列の追加
  "ConnectionStrings": {
    "Database": "DB接続文字列"
  }
}
// Worker.cs

using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SAP.Middleware.Connector;

namespace SapDataCopyApp
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;
        private readonly IHostApplicationLifetime _applicationLifetime;
        // コンストラクタにIConfigurationを追加してフィールドに保持
        private readonly IConfiguration _configuration;

        public Worker(ILogger<Worker> logger, IHostApplicationLifetime applicationLifetime, IConfiguration configuration)
        {
            _logger = logger;
            _applicationLifetime = applicationLifetime;
            _configuration = configuration;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            try
            {
                // (略)

                // DB接続文字列を取得してSQL接続を作成
                string connectionString = _configuration.GetConnectionString("Database");
                using (SqlConnection conn = new SqlConnection(connectionString))
                {
                    await conn.OpenAsync(stoppingToken);
                }
            }
            finally
            {
                // アプリケーションの停止を通知
                _applicationLifetime.StopApplication();
            }
        }
    }
}

DB 接続文字列が正しければ、 ここまでのコードで接続先の SQL Server に対するコネクションがオープンされるはずです。

SqlBulkCopy で BULK INSERT

SQL Server への接続まで完了しましたので、続いて SqlBulkCopy を使用してデータ登録を行います。

SqlBulkCopy でデータを登録するには DataReaderDataTable を用意する必要があり、今回は後者の DataTable を使用する方法で登録します。

まずは DataTable を作成して SAP から取得した通貨一覧を追加します。

// データ登録用のDataTable作成
DataTable dataTable = new DataTable();

// カラム定義の登録
dataTable.Columns.Add("CURRENCY", typeof(string));
dataTable.Columns.Add("CURRENCY_ISO", typeof(string));
dataTable.Columns.Add("ALT_CURR", typeof(string));
dataTable.Columns.Add("VALID_TO", typeof(DateTime));
dataTable.Columns.Add("LONG_TEXT", typeof(string));

// SAPから取得したデータをDataTableに登録
foreach (IRfcStructure row in table)
{
    DataRow dataRow = dataTable.NewRow();

    dataRow["CURRENCY"] = row.GetString("CURRENCY");
    dataRow["CURRENCY_ISO"] = row.GetString("CURRENCY_ISO");
    dataRow["ALT_CURR"] = row.GetString("ALT_CURR");
    dataRow["VALID_TO"] = row.GetValue("VALID_TO");
    dataRow["LONG_TEXT"] = row.GetString("LONG_TEXT");

    dataTable.Rows.Add(dataRow);
}

そして SQL Server に登録するデータが詰まった DataTableSqlBulkCopy を使用して一括転送します。

// DataTableのデータをDBに登録
SqlBulkCopy sqlBulkCopy = new SqlBulkCopy(conn);
sqlBulkCopy.DestinationTableName = "CURRENCY_LIST";
await sqlBulkCopy.WriteToServerAsync(dataTable, stoppingToken);

これで SAP の BAPI を NCo から呼び出して得られたデータを SQL Server に対してかなり効率的に登録することができました。

より効率的な BULK INSERT

SqlBulkCopyDataTable を使用して SQL Server にデータを登録する方法を紹介しました。

この操作は少ないデータ量であれば特に問題はありませんが、何十、何百万というデータを扱う場合にはメモリ使用量とパフォーマンスに大きな問題が発生します。これは DataTable に対してデータを登録する際に行や値を保持するためのオブジェクトを作成することが原因です。

筆者が試した環境では DataTable 作成で SAP から取得されるデータと同程度のメモリ使用量が確認されています。また DataTable 作成にかかる時間は SAP からデータ取得する時間の数倍程度を要しました。

そこで SAP から取得したテーブルを IDataReader のインターフェースを経由してそのまま SqlBulkCopy に渡すことで、 DataTable で発生していた問題を解消できることが期待されます。自作の IDataReader を SqlBulkCopy で使用するにはこちらの記事がとても分かりやすいです。

xin9le.hatenablog.jp

こちらの記事によると SqlBulkCopy で IDataReader を使用するには以下3つのみを実装すれば良いとのことで、実は大変手軽に実装できるようです。

  • int FieldCount
  • object GetValue(int i)
  • bool Read()

それでは早速作ってみましょう。通貨登録先のテーブル情報は分かっていますから、今回は固定でそれを返すだけの DataReader を実装します。

// SapTableReader.cs

using System;
using System.Data;
using SAP.Middleware.Connector;

namespace SapDataCopyApp
{
    /// <summary>
    /// SAPのテーブルをIDataReaderとして扱うためのクラス
    /// </summary>
    public class SapTableReader : IDataReader
    {
        // カラム名と順番の一覧
        private readonly string[] ColumnNames = new[]
        {
            "CURRENCY",
            "CURRENCY_ISO",
            "ALT_CURR",
            "VALID_TO",
            "LONG_TEXT"
        };

        private readonly IRfcTable _table;
        private int _index = -1;

        public SapTableReader(IRfcTable table)
        {
            _table = table;
        }

        /// <summary>
        /// カラム数。今回は固定で5を返す。
        /// </summary>
        public int FieldCount => 5;

        /// <summary>
        /// カラム番号に応じた値をRfcTableから取得する。
        /// </summary>
        public object GetValue(int i)
        {
            // カラム番号に応じたカラム名の取得
            string columnName = ColumnNames[i];

            // 対象のデータカラムの取得
            IRfcStructure row = _table[_index];
            IRfcField col = row[columnName];

            // カラムの値を取得
            switch (col.Metadata.DataType)
            {
                case RfcDataType.DATE:
                    return col.GetValue();

                default:
                    return col.GetString();
            }
        }

        /// <summary>
        /// 次の行を読み込むために内部のインデックスを増加させる。
        /// </summary>
        public bool Read()
        {
            if (_index >= _table.Count - 1)
            {
                return false;
            }

            _index++;
            return true;
        }

        // (略)
    }
}
// Worker.cs

// データ登録用のDataReader作成
SapTableReader reader = new SapTableReader(table);

// DataTableのデータをDBに登録
SqlBulkCopy sqlBulkCopy = new SqlBulkCopy(conn);
sqlBulkCopy.DestinationTableName = "CURRENCY_LIST";
await sqlBulkCopy.WriteToServerAsync(reader, stoppingToken);

大変シンプルですね。たったこれだけのコードで SAP のテーブルを SQL Server に最小限のアロケーションで連携することができます。倍のメモリ使用量も数倍の所要時間も解消され、大変効率的に SAP から SQL Server へとデータを登録することができました。

おわりに

NCo を使用して BAPI を呼び出し、取得されたテーブルを SQL Server に連携する方法について紹介しました。

今回の記事ではカラム数も少ない BAPI_CURRENCY_GETLIST を固定で呼び出しているだけなのでシンプルですが、実際に SAP からデータを取得するにはテーブル数・カラム数共にかなりの数がありますので、とても大変です(大変でした)。

アプリと SAP 間のデータ連係に有償の製品やライブラリを利用されれている方も多いかと思いますが、様々な都合によって自力でデータ連係を組む必要がある場合に、本記事が手助けになれば幸いです。

執筆担当者プロフィール
中谷 大造

中谷 大造(日本ビジネスシステムズ株式会社)

情報システム部の中谷です。社内用スクラッチアプリの開発をしています。

担当記事一覧