ExcelからDynamoDBのデータを取得するDLLを作ってみた話

# はじめに
ExcelからDynamoDBにアクセスしてデータを取得するツールを作ってみた。  
QiitaなどWEB上に記事を投稿するのは初めてなので、練習兼備忘として色々書いてみる。

# 実現方法を考える
DynamoDBのデータを取得するには色々な手段があるが、今回はExcelを起点とするためVBAから実行できる処理が必要だった。  
AWS系のサービスにプログラムからアクセスするとき真っ先に思いつくのはAWS SDKだが、VBA用のものは見当たらない。  
他の手段としてはREST APIでデータを取得出来そうだったが、めんどくさそうな気配がしたのでやりたくない…  
結果、AWS SDKが用意されているC#.NETを用いてVBAから呼び出せるDLLを作成し、それ経由でデータを取得することにした。  

# とりあえず作業開始
C#.NETで書くと言っても、実はJavaエンジニアな私はサラッと書けず、ネット上の記事を参考にしながらのコーディングだった。  
Visual Studio 2022をインストールし、C#用のクラスライブラリ(.NET Framework)プロジェクトのテンプレートを使ってプロジェクトを作成する。  
![プロジェクトテンプレート選択画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492482/5d89b923-d77d-08da-7de1-f15c62218ee7.png)

## プロジェクトプロパティを変更する
次にDLLを使用するための設定を行う。（あまり理解できてないが、これを行わないとDLLとして使えないらしい）  
画面最上部のメニューバーから、「プロジェクト」 > 「（プロジェクト名）のプロパティ」をクリックすると下記のような画面が開くので2カ所設定を変える。  
  
まずは「アプリケーション」タブから「アセンブリ情報」を開き、「アセンブリをCOM参照可能にする」にチェックを入れる。  
![プロパティ_アプリケーション](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492482/8b8dc8ba-f361-8bfb-f50a-bab5103344cf.png)
![プロパティ_アプリケーション_アセンブリ情報](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492482/e2f27276-48bf-8da5-2946-5a39d117c758.png)

次に、「ビルド」タブを開き「COM相互運用機能の登録」にチェックを入れる。  
※これに関しては必須ではなく、別途コマンドを打てば代替可能らしい。
![プロパティ_ビルド](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492482/423e2085-d184-6d5e-9b85-72ad699356b2.png)


設定し終えたら`Ctrl + S`で設定を保存して完了だ。

## AWS SDKのインストール
続いて、DynamoDBにアクセスするためのAWS SDKをインストールする。  
画面最上部のメニューバーから、「プロジェクト」 > 「NuGetパッケージの管理」をクリックすると下記のような画面が開く。  
![NuGetパッケージマネージャー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492482/6f17ec87-83f1-0a12-f59d-4090596b6307.png)



検索欄に`dynamodb`と入力して、`AWSSDK.DynamoDBv2`をインストールする。  
依存している必須パッケージである`AWSSDK.Core`は同時にインストールされるので別途インストールする必要はない。  
![NuGetパッケージマネージャー_dynamodb](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492482/fcc5c508-ba56-c81b-8d31-10a51515eadc.png)


## コーディングしてみる
必須のセットアップは完了したので、拙いながらもコーディングをしてみる。  


```c#:GetData.cs
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.Runtime;

namespace DynamoDBAccessQiita
{
    [ComVisible(true)]
    public interface IDynamoDBAccessQiita
    {
        [Description("DynamoDBから設定値を取得する")]
        List<string[]> getDynamoDBData(string partitionKeyValue);
    }

    [ClassInterface(ClassInterfaceType.None)]
    public class GetData : IDynamoDBAccessQiita
    {

        private static readonly string[] attributeNames = new string[]
        {
            "PKey",
            "SKey",
            "Col1",
            "Col2"
        };

        public List<string[]> getDynamoDBData(string partitionKeyValue)
        {
            AmazonDynamoDBClient dynamoDBClient = new AmazonDynamoDBClient(
                new BasicAWSCredentials("AccessKeySample", "SecretKeySample"),
                new AmazonDynamoDBConfig
                {
                    RegionEndpoint = Amazon.RegionEndpoint.APNortheast1
                });

            // クエリの作成
            QueryRequest request = new QueryRequest
            {
                TableName = "SampleTable",
                KeyConditionExpression = "#pk = :pk",
                ExpressionAttributeNames = new Dictionary<string, string>
                {
                    { "#pk", "PKey" }
                },
                ExpressionAttributeValues = new Dictionary<string, AttributeValue>
                {
                    { ":pk", new AttributeValue { S = partitionKeyValue } }
                }
            };

            // クエリの実行
            QueryResponse response = dynamoDBClient.Query(request);

            var result = new List<string[]>();

            // 取得したレコードを返却用に詰め替え
            foreach (var item in response.Items)
            {
                var record = new string[attributeNames.Length];

                for (int i = 0; i < attributeNames.Length; i++)
                {
                    record[i] = item.TryGetValue(attributeNames[i], out var value) ? value.S : null;
                }

                result.Add(record);
            }

            return result;
        }

    }
}
```
説明用のコードなので簡略化しているが、本来はもう少しまともなコードなのでお目汚しご容赦願いたい。  
この状態でビルドすると以下のような警告が出る。  
```
warning : タイプ ライブラリ エクスポーターで '○○○○' を処理中に警告が出されました。警告: タイプ ライブラリ エクスポーターでシグネチャのジェネリック型インスタンスが見つかりました。ジェネリック コードを COM にエクスポートすることはできません。
```
分からない単語だらけで解決に時間がかかったが、`ジェネリック型`というのが諸悪の根源のようだ…  

## ジェネリック型を使うことはできない
前述のコードは、取得したデータを`List`に格納して呼び出し元に返そうとするコードである。  
この`List`、よくよく見てみると名前空間が`System.Collections.Generic`となっている。  
どうやらこれに属しているクラスが`ジェネリック型`と呼ばれるものらしい。
`List<string[]>`のように要素の型を指定出来るものであり、普段List構造を使うならこの書き方だろう。  
しかし、DLLとしては`List`で返すことができないようなので、非ジェネリック型である`ArrayList`を使うことでこの問題を解消できる。  
違いについては別途調べてもらいたいが、大して違いはなさそうなので単純に置き換えてみる。  

```c#:GetData.cs
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.Runtime;

namespace DynamoDBAccessQiita
{
    [ComVisible(true)]
    public interface IDynamoDBAccessQiita
    {
        [Description("DynamoDBから設定値を取得する")]
        ArrayList getDynamoDBData(string partitionKeyValue);
    }

    [ClassInterface(ClassInterfaceType.None)]
    public class GetData : IDynamoDBAccessQiita
    {

        private static readonly string[] attributeNames = new string[]
        {
            "PKey",
            "SKey",
            "Col1",
            "Col2"
        };

        public ArrayList getDynamoDBData(string partitionKeyValue)
        {
            AmazonDynamoDBClient dynamoDBClient = new AmazonDynamoDBClient(
                new BasicAWSCredentials("AccessKeySample", "SecretKeySample"),
                new AmazonDynamoDBConfig
                {
                    RegionEndpoint = Amazon.RegionEndpoint.APNortheast1
                });

            // クエリの作成
            QueryRequest request = new QueryRequest
            {
                TableName = "SampleTable",
                KeyConditionExpression = "#pk = :pk",
                ExpressionAttributeNames = new Dictionary<string, string>
                {
                    { "#pk", "PKey" }
                },
                ExpressionAttributeValues = new Dictionary<string, AttributeValue>
                {
                    { ":pk", new AttributeValue { S = partitionKeyValue } }
                }
            };

            // クエリの実行
            QueryResponse response = dynamoDBClient.Query(request);

            var result = new ArrayList();

            // 取得したレコードを返却用に詰め替え
            foreach (var item in response.Items)
            {
                var record = new string[attributeNames.Length];

                for (int i = 0; i < attributeNames.Length; i++)
                {
                    record[i] = item.TryGetValue(attributeNames[i], out var value) ? value.S : null;
                }

                result.Add(record);
            }

            return result;
        }

    }
}
```
再度ビルドしてみると、今度はエラーも警告もなしで完了した。  
一旦DLL側はこの状態で置いておいて、VBA側の対応を行う。

## VBA側でやること
まずはDLLをVBAから呼び出せるようにする。  
VBAのエディタを開き、画面上部のメニューバーから、「ツール」 > 「参照設定」をクリックすると下記のような画面が開くので、先ほど作成したDLLにチェックを入れてOKボタンを押下するだけで良い。  
![VBA_参照設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492482/6625f0a6-08c0-43a8-810d-cbfa7fb4f9ab.png)


次にDLL経由でDynamoDBのデータを取得し、シートに書き出すマクロを書く。
```vb:VBA
Option Explicit

Sub GetDynamoDB()

    Dim dll As DynamoDBAccess.GetData
    Set dll = New DynamoDBAccess.GetData
    
    Dim records As Object
    Dim record As Variant
    Dim rowIndex As Integer
    
    ' 外部DLL経由でDynamoDBの値を取得
    Set records = dll.getDynamoDBData("hoge")
    
    ' 取得したリストをシートに出力
    For Each record In records
        ActiveSheet.Range("A1").Offset(rowIndex).Resize(, 4) = record
        rowIndex = rowIndex + 1
        
    Next record

End Sub
```
試しに動かしてみたらちゃんと取得できたので、今回はここまで。  

# 最後に
本来であれば、取得処理部分では対象のデータ型ごとに異なる処理をしなければならないのだが、今回は説明用に文字列だけを対象としたコードにしている。  
また、使用しているExcelが32ビット用か64ビット用かでもビルドオプションを変えなければならないなど、説明を省略している部分が多々ある。  
今後もDLLを作成する機会があれば、そのあたりも補足を入れて記事にしたい。