【VBA】ADOを使用してExcel表をDB操作する方法のまとめ（範囲指定、書き込みなど）

# はじめに

この記事は、**ADO**（ActiveX Data Objects）を使用して、**Excelファイル**を**SQL文で操作する**方法のまとめです。
大半はよくある内容ですが、ワークシートの読み込みから、DBとしての更新処理まで、自身の備忘を兼ねて残しておきます。

なお、**ADO**（ActiveX Data Objects）とは「**データベース操作のためのパッケージ（API）**」のことです（詳しくは[IT用語辞典 ADO](http://e-words.jp/w/ADO.html)を参照してください）。

**＜目次＞**
[1. 基本的なサンプルコード](#1-基本的なサンプルコード)
　[1-1. シート全体をテーブルとして取得する場合](#1-1-シート全体をテーブルとして取得する場合)
　[1-2. シートの一部をテーブルとして取得する場合](#1-2-シートの一部をテーブルとして取得する場合)
[2. 各コードの説明](#2-各コードの説明)
　[2-1. 外部ライブラリの機能を持ったオブジェクト変数（インスタンス）の作成](#2-1-外部ライブラリの機能を持ったオブジェクト変数インスタンスの作成)
　[2-2. ADO接続](#2-2-ado接続)
　[2-3. SQL文で抽出したデータをレコードセットで開く](#2-3-sql文で抽出したデータをレコードセットで開く)
　[2-4. レコードセットの中身を確認する](#2-4-レコードセットの中身を確認する)
[3. ワークシートのデータを更新する方法](#3-ワークシートのデータを更新する方法)
　[3-1. ファイルを開いてレコードを書き込む（CopyFromRecordsetメソッド）](#3-1-ファイルを開いてレコードを書き込むcopyfromrecordsetメソッド)
　[3-2. ファイルを開かずにレコードを書き込む（Update、UpdateBatchメソッド）](#3-2-ファイルを開かずにレコードを書き込むupdateupdatebatchメソッド)

# 1. 基本的なサンプルコード

## 1-1. シート全体をテーブルとして取得する場合

まず、基本として、ワークシート全体をテーブルとして取得してSQL文で操作する場合です。
次のようなExcel表をサンプルとして使用します。
![2020-11-08 212220.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/fb8ee94a-6992-9b52-7e4f-ffc46016bb0e.png)
この記事においては、「表を記載したファイル」と「VBAを記載するファイル」は別々にして、同じフォルダに保存しています。
参考までに、私の手元では、次のような構成としています。

|用途|ファイル名|ディレクトリ（フルパス）|
|---|---|---|
|VBAを記録するファイル|ADOTest.xlsm|C:\VBA\ADOTest.xlsm|
|表を記載するファイル|TestTable.xlsx|C:\VBA\TestTable.xlsx|

### 1-1-1. 参照設定をしない場合

最初に、[参照設定](https://excel-ubara.com/excelvba4/EXCEL296.html)をしない場合の例です。
つまり、VBAで事前の設定をしなくても書けるコードです。

```vb:サンプルコード1（参照設定をしない場合）
Sub Sample1()
    '外部ライブラリからインスタンスを作成（実行時バインディング）
    Dim cn As Object
    Dim rs As Object
    Set cn = CreateObject("ADODB.Connection")
    Set rs = CreateObject("ADODB.Recordset")
    
    'ADO接続
    cn.Provider = "Microsoft.ACE.OLEDB.12.0"
    cn.Properties("Extended Properties") = "Excel 12.0"
    cn.Open ThisWorkbook.Path & "\" & "TestTable.xlsx"
    
    'SQL文の実行
    rs.Open "SELECT * FROM [Sheet1$] ORDER BY 単価", cn
    
    '取得した内容（Recordset）の確認
    Do Until rs.EOF
        Debug.Print rs!品名 & ", " & rs!単価
        rs.MoveNext
    Loop
    
    'メモリの解放（無くとも構わない）
    rs.Close
    Set rs = Nothing
    cn.Close
    Set cn = Nothing
End Sub
```

Excel表の1行目部分を自動的にカラム名として読み取って、レコードセット（`rs`）に格納しています。
レコードの内容は、`rs![カラム名]`という形で取得することができます。
詳細は後述します。

＜出力結果＞

```:イミディエイト
みかん, 40
じゃがいも, 50
にんじん, 70
りんご, 150
キャベツ, 180
バナナ, 300
メロン, 1500
```

SQL文の`ORDER BY句`で、単価の昇順に並べ替えがされています。

[イミディエイト ウィンドウ](https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/use-the-immediate-window)が表示されない場合は、VBAのメニュー「表示」から「イミディエイト ウィンドウ」を選択してください。

### 1-1-2. 参照設定をする場合

次に、[参照設定](https://excel-ubara.com/excelvba4/EXCEL296.html)をする場合の例です。
参照設定をするには、VBAメニューの「ツール(T)」から「参照設定(R)」を開きます。
![2020-11-08 122610.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/79a87764-b68e-c379-a0a1-dd23153ecd6a.png)
参照設定の画面で、次のように`Microsoft ActiveX Data Object X.X Library`をチェックして`OK`ボタンを押せば完了です（ここではバージョンは`6.1`を選択しています）。
![2020-11-08 122928.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/dac58aaf-7b2f-e428-a178-307e757138e0.png)
この設定により、プログラムの実行前に、`ActiveX Data Object (ADO)`ライブラリを読み込むことになります。

記載するソースコードは、以下のとおりです。
前の例と異なるのは、最初の2行の`インスタンス作成部分`だけです。
細かい説明は後述します。

```vb:サンプルコード2（参照設定をする場合）
Sub Sample2()
    'インスタンスを作成（事前バインディングの場合）
    Dim cn As New ADODB.Connection
    Dim rs As New ADODB.Recordset
    
    'ADO接続
    cn.Provider = "Microsoft.ACE.OLEDB.12.0"
    cn.Properties("Extended Properties") = "Excel 12.0"
    cn.Open ThisWorkbook.Path & "\" & "TestTable.xlsx"
    
    'SQL文の実行
    rs.Open "SELECT * FROM [Sheet1$] ORDER BY 単価", cn
    
    '取得した内容（Recordset）の確認
    Do Until rs.EOF
        Debug.Print rs!品名 & ", " & rs!単価
        rs.MoveNext
    Loop
    
    'メモリの解放（無くとも構わない）
    rs.Close
    Set rs = Nothing
    cn.Close
    Set cn = Nothing
End Sub
```

（出力結果は同じなので省略）

## 1-2. シートの一部をテーブルとして取得する場合

次に、ワークシートの一部をテーブルとして取得してSQL文で操作する場合です。
次のようなExcel表をサンプルとして使用します。
![2020-11-08 212508.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/3769a7da-b7a6-8042-9a71-fd6523f8b7cf.png)
このような場合は、赤枠の部分（B4からF11の間のデータ）をテーブルとして取得する必要があります。
コードを書くと次のとおりです。

```vb:サンプルコード3（ワークシートの範囲を指定する場合）
Sub Sample3()
    'インスタンスを作成（事前バインディングの場合）
    Dim cn As New ADODB.Connection
    Dim rs As New ADODB.Recordset
    
    'ADO接続
    cn.Provider = "Microsoft.ACE.OLEDB.12.0"
    cn.Properties("Extended Properties") = "Excel 12.0"
    cn.Open ThisWorkbook.Path & "\" & "TestTable.xlsx"
    
    'SQL文の実行（シートの範囲を指定してテーブルとする）
    rs.Open "SELECT * FROM [Sheet1$B4:F] WHERE 区分 = '果物' ORDER BY 単価 DESC", cn
    
    '取得した内容（Recordset）の確認
    Do Until rs.EOF
        Debug.Print rs!品名 & ", " & rs!単価
        rs.MoveNext
    Loop
    
    'メモリの解放（無くとも構わない）
    rs.Close: Set rs = Nothing
    cn.Close: Set cn = Nothing
End Sub
```

＜出力結果＞

```:イミディエイト
メロン, 1500
バナナ, 300
りんご, 150
みかん, 40
```

SQL文の絞り込み方法を変えているので、出力結果は変わりますが、正しくテーブルの範囲を指定できていることがわかります。

**＜ソースコードの変更部分＞**
ソースコードのうち、表の範囲を指定しているのは次の１行です。

```vb:テーブル範囲の指定部分
rs.Open "SELECT * FROM [Sheet1$B4:F] WHERE 区分 = '果物' ORDER BY 単価 DESC", cn
```

ページ全体を取得する場合は、テーブルを`[Sheet1$]`と指定していたのに対して、範囲を指定する場合は`[Sheet1$B4:F]`というように、`$`の後に、対象となるセルの範囲の**ヘッダー部分**を指定しています。
この場合、表の下限は自動的に判定されます。

なお、`[Sheet1$B4:F11]`というように、表の下限まで指定することもできますが、レコードの追加時にエラーが出てしまうなど、汎用性が落ちます。

また、テーブルの上側及び左側が全て空白の場合は`[Sheet1$]`と指定しても、自動的にテーブルの範囲を取得してくれます（本例の場合は、`B2`セルに"●買物メモ"と入っているので自動取得はできません）。

# 2. 各コードの説明

以下、外部ライブラリとかインスタンスとか、慣れない人には分からない用語が出てきますが、言葉の意味は重要ではないので気にしなくても大丈夫です（簡単に説明は入れていきます）。
用語には、「[Microsoft](https://www.microsoft.com/ja-jp/)」の公式ページか、「[エクセルの神髄](https://excel-ubara.com/)」というサイト（かなり参考になるサイトです）のリンクを付けさせていただきます。

## 2-1. 外部ライブラリの機能を持ったオブジェクト変数（インスタンス）の作成

最初に、各サンプルコードの冒頭部分「外部ライブラリを利用したインスタンス生成」の部分についてです。
`外部ライブラリを使用する`ということは、VBA自体の機能ではなくて、[ActiveX](https://ja.wikipedia.org/wiki/ActiveX)という機能（外部ライブラリ）を使用することを意味します。
ここは、参照設定の有無で記載が分かれますので、順に見ていきます。

まずは、参照設定をしない場合の例です。

```vb:参照設定をしない場合
Dim cn As Object
Dim rs As Object
Set cn = CreateObject("ADODB.Connection")
Set rs = CreateObject("ADODB.Recordset")
```

ここでは、プログラムの実行時に[CreateObject関数](https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/createobject-function)で外部ライブラリを呼び出して、その機能を有するオブジェクト変数（`cn`、`rs`）を作成しています。
このようなライブラリの機能（実体）を持ったオブジェクト（変数）をインスタンスと呼んでいます。

次に、参照設定をした場合です。

```vb:参照設定をした場合
Dim cn As New ADODB.Connection
Dim rs As New ADODB.Recordset
```

プログラムの実行前に`ActiveX Data Object (ADO)`ライブラリを読み込んでいるため、直接、`ADODB.Connection`型および`ADODB.Recordset`型のオブジェクト変数を宣言をすることができます。
オブジェクト変数の前に`New`を付けることで、変数の宣言と同時にインスタンスを生成しています。

## 2-2. ADO接続

次に、以下の3行についてです。

```vb:Office2007以降の場合
cn.Provider = "Microsoft.ACE.OLEDB.12.0"
cn.Properties("Extended Properties") = "Excel 12.0"
cn.Open ThisWorkbook.Path & "\" & "TestTable.xlsx"
```

上記の3行で、対象ファイルとの`接続`を確立しています。
これにより、ADOによるデータベース操作が可能となります。
以下、順次、1行ずつ見ていきます。

### 2-2-1. プロバイダ（ドライバ）の指定

```vb
cn.Provider = "Microsoft.ACE.OLEDB.12.0"
```

まず、1行目の`cn.Provider = `の部分では、ADO接続に使用するプロバイダ（ドライバ）の呼び出しを行っています。
基本的には、Officeのバージョンに合わせて、次のように指定すれば大丈夫です。

|指定文字列|内容|
|---|---|
|Microsoft.Jet.OLEDB.4.0|Office2003以前の場合|
|Microsoft.ACE.OLEDB.12.0|Office2007以降の場合|

参考記事
・[Microsoft Access（.mdb、.accdb）のODBC、OLEDBドライバーに関するまとめ](https://qlik-training.ashisuto.co.jp/microsoft-access-odbc-oledb-driver/)
・[ ADOによるアクセスの仕組み](http://www.shoeisha.com/book/hp/pc/office/ADOExcel/files/text2.html)

### 2-2-2. 拡張プロパティ（Extended Properties）の指定

```vb:
cn.Properties("Extended Properties") = "Excel 12.0"
```


2行目の`cn.Properties("Extended Properties") =`の部分では、拡張プロパティ（Extended Properties）についての指定を行っています。
この拡張プロパティについては、実は、はっきり分からないことも多いのですが、「[ADO.NET / 接続文字列の構文](https://docs.microsoft.com/ja-jp/dotnet/framework/data/adonet/connection-string-syntax)」など、いくつかのサイトを参考にさせていただきました。
基本的には「**データベースでないもの（Excelやcsvファイル）をデータベースとして扱う場合の設定**」を行うところとなります。

**① ISAMバージョンの指定**
[ISAM（Indexed Sequential Access Method）](https://ja.wikipedia.org/wiki/Indexed_Sequential_Access_Method)とは、日本語で言えば「索引付き順次アクセス方式」となります。
次の例にならえば、問題はないようです。

|指定文字列|内容|
|---|---|
|Excel 8.0|Office2003以前の場合|
|Excel 12.0|Office2007以降の場合|
|Text|csvファイルの場合|

**② 1行目をカラム名（ヘッダー）として取得するか否かの指定**
この指定をしない場合は、デフォルトで`HDR=Yes`（1行目をカラム名として取得する）となります。

|指定文字列|内容|
|---|---|
|HDR=Yes|1行目をカラム名として取得する|
|HDR=No|1行目はカラム名として取得しない|

**③ IMEX（IMPORT EXPORT MODE）の設定**
この`IMEX`は、サンプルコードのように特に指定しない例が多いです。
次のようなモードが用意されています。

|指定文字列|内容|
|---|---|
|IMEX=0|エクスポートモード（書き込みモード）|
|IMEX=1|インポートモード（読み込みモード）|
|IMEX=2|リンクモード（書き込みと読み込み）|

もし、データの読み込みだけに限るならば`IMEX=1`としている例も多く見られます。この読み込みモードでは、混在したデータはテキストとして読み込んでくれるとの[解説](https://docs.microsoft.com/ja-jp/dotnet/framework/data/adonet/connection-string-syntax#connecting-to-excel)もあります（しかし、実際は思うような挙動が得られなかったりしますので、難しいところです）。

なお、`IMEX=0`（書き込みモード）と設定した場合には、ファイルを開くことなく、AccessのようにDBとしての書き込みが可能になります（[後述](#3-2-ファイルを開かずにレコードを書き込むupdateupdatebatchメソッド)）。

**○ 複数の項目を指定する方法**
以上の**①**から**③**以外にも設定できる内容があるようですが、ここでは触れません（私が、あんまり分かっていません）。
複数の項目を指定する場合は、次のように`;`で繋げて記載することになります。

```vb:複数のExtendedPropertiesを設定する例
cn.Properties("Extended Properties").Value = "Excel 12.0;HDR=Yes;IMEX=1"
```

**＜参考サイト＞**
・[ADO.NET入門記-027 (Excelに接続してデータを取得・追加・更新)](https://devlights.hatenablog.com/entry/20091116/p1)
・[OLEDB接続文字列内のIMEXとは何ですか？](https://www.it-swarm-ja.tech/ja/c%23/oledb%E6%8E%A5%E7%B6%9A%E6%96%87%E5%AD%97%E5%88%97%E5%86%85%E3%81%AEimex%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%A7%E3%81%99%E3%81%8B%EF%BC%9F/1067377833/)
・[What is the default value of IMEX in OLEDB?](https://stackoverrun.com/ja/q/3020246)
・[关于Excel导入的HDR=YES; IMEX=1详解](https://www.cnblogs.com/goto/archive/2012/04/12/2443670.html)


### 2-2-3. ファイルを指定して接続を実行する
```vb:
cn.Open ThisWorkbook.Path & "\" & "TestTable.xlsx"
```

3行目の`cn.Open`のところでは、データベースとして開くファイルのディレクトリ（`ThisWorkbook.Path & "\" & "TestTable.xlsx"`）を指定して、接続処理の実行を行っています。
これは、同じフォルダ内の`TestTable.xlsx`という別のExcelファイルを指定している例です。

VBAを記載したファイル自体にあるワークシートを参照するならば、次のようにすれば参照できます。

```vb:VBA記載ファイル内のワークシートを参照する場合
cn.Open ActiveWorkbook.FullName
```

なお、次のように、「ディレクトリの指定」と「接続の実行」を分けて記載することもできます（迂遠ですが）。

```vb:ディレクトリの指定を個別に行う場合
cn.Properties("Data Source") = ThisWorkbook.Path & "\" & "TestTable.xlsx"
cn.Open
```

**＜注意＞**
"\"の部分は環境（フォント）によって、半角の`＼`または`￥`と2通りの表示がされます。
コピペすれば、VBAの画面上では"&yen;"と表示されるはずです。

### 2-2-4. ADO接続を1行で表記する方法

以上のように、本例では、3行に分けて記載しましたが、次のように1行にまとめて記載することも可能です（普通は適当な位置で折り返しを入れます）。

```vb:ADO接続を1行で記載する場合（自ファイル内のワークシートを参照の例）
cn.Open "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & ActiveWorkbook.FullName & ";Extended Properties=""Excel 12.0;HDR=No"""
```

## 2-3. SQL文で抽出したデータをレコードセットで開く

レコードセット（Recordset）は、レコード（行）とフィールド（列）から構成され、表のようなデータ構造をしています。
この[Recordsetオブジェクト](https://docs.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/recordset-object-ado)の「[Openメソッド](https://docs.microsoft.com/ja-jp/sql/ado/reference/ado-api/open-method-ado-recordset?view=sql-server-ver15)」を使用して、SQL文で抽出したデータやテーブルなどを開いて操作をすることができます。

サンプルコードでは、次のように`SQL文`と`コネクション`のみを指定していますが、その他にもパラメータ（引数）があります。

```vb
rs.Open "SELECT * FROM [Sheet1$] ORDER BY 単価", cn
```

このOpenメソッドの全てのパラメーターを含めた正式な構文を書くと次のようになります。

```:Openメソッドの構文
recordset.Open Source, ActiveConnection, CursorType, LockType, Options
```

Openメソッドのパラメーターを一覧にすると次のとおりです（ざっくりな説明です）。

|パラメーター|日本語|説明|
|---|---|---|
|Source|DBのソース|SQLステートメント、テーブル名などを指定|
|ActiveConnection|コネクション|DBへの接続（コネクション）を指定|
|[CursorType](https://docs.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/cursortype-property-ado)|カーソルの種類|カーソルの移動や、他のユーザーの更新結果を反映するかを指定（adOpenForwardOnly、adOpenKeyset、adOpenDynamic、adOpenStatic）|
|[LockType](https://docs.microsoft.com/ja-jp/sql/ado/reference/ado-api/locktype-property-ado?view=sql-server-ver15)|ロックの種類 |編集中にレコードに適用されるロックの種類を指定（adLockBatchOptimistic、adLockOptimistic、adLockPessimistic、adLockReadOnly、adLockUnspecified）|
|Options|オプション||

パラメーターの詳細は、「[3-2. ファイルを開かずにレコードを書き込む（Update、UpdateBatchメソッド）](#3-2-ファイルを開かずにレコードを書き込むupdateupdatebatchメソッド)」で再度取り上げます。

## 2-4. レコードセットの中身を確認する

サンプルコードでは、次のループ処理でレコードを１行ずつ読み込んでいます。

```vb
Do Until rs.EOF
    Debug.Print rs!品名 & ", " & rs!単価
    rs.MoveNext
Loop
```

このコードでは、レコードが末尾まで行くと、`rs.EOF`がTrueになるので、それまでの間ループを繰り返します。
なお、EOFは`End Of File`のことです（また、BOFは`Beginning Of File`のことです）。


そして、現在カーソルがあるレコードのデータを抽出するには、`rs![カラム名]`という形で取得することができます。
そのほかの書き方としては、`rs([カラム名])`でも、同様にデータを取得できます（次のとおり）。

```vb
Debug.Print rs("品名") & ", " & rs("単価")
```

レコードを読み込んだ後、次のレコードには自動的に移らないので、`rs.MoveNext`というように`MoveNextメソッド`を使用してカーソルを移動させています。

レコードセットの主なメソッドおよびプロパティは次のとおりです。

|名称|内容|
|---|---|
|[MoveFirstメソッド](https://docs.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/movefirst-movelast-movenext-and-moveprevious-methods-ado)|レコード位置を最初のレコードに移動|
|[MoveLastメソッド](https://docs.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/movefirst-movelast-movenext-and-moveprevious-methods-ado)|レコード位置を最後のレコードに移動|
|[MoveNextメソッド](https://docs.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/movefirst-movelast-movenext-and-moveprevious-methods-ado)|レコード位置を1レコード次に移動|
|[MovePreviousメソッド](https://docs.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/movefirst-movelast-movenext-and-moveprevious-methods-ado)|レコード位置を1レコード前に移動|
|[Updateメソッド](https://docs.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/update-method-ado)|レコードの更新を保存|
|[UpdateBatchメソッド](https://docs.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/updatebatch-method-ado)|保留中の更新を一括して保存|
|[AddNewメソッド](https://docs.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/addnew-method-ado)|新しいレコードを作成|
|[Deleteメソッド](https://docs.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/delete-method-ado-recordset)|レコードを削除|
|[Findメソッド](https://docs.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/find-method-ado)|指定した条件を満たす行を検索|
|[BOFプロパティ](https://docs.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/bof-eof-properties-ado)|レコードの位置が最初のレコードより前にあることを示す|
|[EOFプロパティ](https://docs.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/bof-eof-properties-ado)|レコードの位置が最後のレコードより後にあることを示す|

# 3. ワークシートのデータを更新する方法

Excelファイルへのデータ更新は、普通に Excel VBA の手法で書き換えることができます。
ただ、ここでは`Recordset`を使用して、Excelファイル（DB）を更新する方法について書いておきます。

なお、更新方法には、「**ファイルを開いてRecordsetのデータを貼り付ける単純な方法**」と「**ファイルを開かず一般的なDBのように更新する方法**」がありますので、これらの例を、順を追って紹介させていただきます。

## 3-1. ファイルを開いてレコードを書き込む（CopyFromRecordsetメソッド）

まず、ファイルを開いてレコードを書き込む方法ですが、Excelを操作する上では、比較的に安定していて使いやすい手法だと思います。

```vb:サンプルコード4
Sub Sample4()
    'インスタンスを作成（事前バインディング）
    Dim cn As New ADODB.Connection
    Dim rs As New ADODB.Recordset
    
    Dim wb As Workbook
    Set wb = Workbooks.Open(ThisWorkbook.Path & "\" & "TestTable.xlsx")
    
    'ADO接続
    cn.Provider = "Microsoft.ACE.OLEDB.12.0"
    cn.Properties("Extended Properties") = "Excel 12.0"
    cn.Open ThisWorkbook.Path & "\" & "TestTable.xlsx"
    
    
    'SQL文の実行
    rs.Open "SELECT * FROM [Sheet1$] ORDER BY 単価", cn, adOpenKeyset
    
    'Recordsetのデータを貼り付ける
    wb.Sheets("Sheet1").Cells(2, 1).CopyFromRecordset rs
    
    'ファイルを保存して閉じる
    wb.Close savechanges:=True
    
    'メモリの解放（無くとも構わない）
    rs.Close: Set rs = Nothing
    cn.Close: Set cn = Nothing
End Sub
```

このコードを実行すると、単価の昇順でデータが並べ替えられます。

[CopyFromRecordsetメソッド](https://docs.microsoft.com/ja-jp/office/vba/api/excel.range.copyfromrecordset)を使っているのは次の部分です。

```vb
wb.Sheets("Sheet1").Cells(2, 1).CopyFromRecordset rs
```

ワークシートのうち、セルを貼り付ける左上の起点を指定して（`Cells(2, 1)`）、レコードセットの行数および列数分だけデータを貼り付けます。

## 3-2. ファイルを開かずにレコードを書き込む（Update、UpdateBatchメソッド）

次に、ファイルを開かずに、一般的なDBのようにデータを更新する方法です。

### 3-2-1. レコードセットと連動して更新を行う

ますは、RecordsetのUpdateメソッドを使用して、レコードを1行ずつ更新する方法です。

#### 3-2-1-1. レコードを1行ずつ更新する

```vb:サンプルコード5（Recordsetと連動してデータ更新）
Sub Sample5()
    'インスタンスを作成（事前バインディング）
    Dim cn As New ADODB.Connection
    Dim rs As New ADODB.Recordset
    
    'ADO接続
    cn.Provider = "Microsoft.ACE.OLEDB.12.0"
    cn.Properties("Extended Properties") = "Excel 12.0;HDR=Yes;IMEX=0"
    cn.Open ThisWorkbook.Path & "\" & "TestTable.xlsx"
    
    'SQL文の実行
    rs.Open "SELECT * FROM [Sheet1$] WHERE 区分 = '野菜'", cn, adOpenStatic, adLockPessimistic
    
    'Recordsetの更新
    Do Until rs.EOF
        rs!単価 = Val(rs!単価) + 1
        rs.Update
        rs.MoveNext
    Loop
    
    'メモリの解放（無くとも構わない）
    rs.Close: Set rs = Nothing
    cn.Close: Set cn = Nothing
End Sub
```

これは、区分が`野菜`であるレコードについて、単価を１円だけ上げる例ですが、Recordsetを1行更新するたびに、Excelのワークシートも同時に更新されていきます。
これにより、一般的なDBと同じような形で操作することができます。

このコードで重要な点の一つは、ADO接続の`拡張プロパティ（Extended Properties）`の部分です（次のところ）。

```vb
cn.Properties("Extended Properties") = "Excel 12.0;HDR=Yes;IMEX=0"
```

IMEXのところが`IMEX=0`となっています。これによりExcelファイルは`書き込みモード（エクスポートモード）`になり、Recordsetから更新することが可能になります。

もう一つ重要なところは、RecordsetのOpenメソッドのところです。

```vb
rs.Open "SELECT * FROM [Sheet1$] WHERE 区分 = '野菜'", cn, adOpenStatic, adLockPessimistic
```

ここでは、カーソルタイプ（[CursorTypeプロパティ](https://docs.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/cursortype-property-ado)）とロックタイプ（[LockTypeプロパティ](https://docs.microsoft.com/ja-jp/sql/ado/reference/ado-api/locktype-property-ado?view=sql-server-ver15)）を指定しています。

それぞれの種類を見てみると次のようになっています。

**① カーソルタイプ（CursorType）**

|名称|日本語|内容|
|---|---|---|
|adOpenDynamic|動的カーソル|他のユーザーによる追加・変更・削除が反映される|
|adOpenKeyset|キーセットカーソル|他のユーザーによる変更は反映されるが、追加・削除したレコードにはアクセスできない|
|adOpenStatic|静的カーソル|他のユーザーによる追加・変更・削除は反映されない|
|adOpenForwardOnly|順方向専用カーソル|他のユーザーによる追加・変更・削除は反映されない（パフォーマンス性は向上する）|

Excelファイルへの複数ユーザーの同時アクセスを考慮する機会はあまりないと思いますが、カーソルタイプによりRecordsetで取得できるデータが変わってきます（本稿では、ほぼ考慮していません）。

**② ロックタイプ（LockType）**

|名称|日本語|
|---|---|
|adLockBatchOptimistic|バッチ更新用の共有的ロック|
|adLockOptimistic|共有的ロック|
|adLockPessimistic|排他的ロック|
|adLockReadOnly|読み取り専用|
|adLockUnspecified|ロック指定なし|

本例では、2番目の`adLockOptimistic`（共有ロック）と、3番目の`adLockPessimistic`（排他的ロック）であればデータの更新ができます。
1番目の`adLockBatchOptimistic`は、後述のバッチ更新で使用します。
なお、4番目の`adLockReadOnly`（読み取り専用）、5番目の`adLockUnspecified`（指定なし）は、データ更新する場合には基本的には使用しません。

このあたりの詳細は「[ロック機構 - Windows 2000 - Development Guide for Business Applications](https://www.itmedia.co.jp/help/howto/win/win2000/0007special/complus_vb/chap1/05.html)」および「[CursorTypeプロパティ](http://yasusigi.net/iasdocs/aspdocs/ref/comp/daprop02_7.htm)」などの記事で分かりやすく解説されています。

最後にUpdateメソッドについてです。
Recordsetを更新するたびに、次のように`Updateメソッド`（`rs.Update`のところ）を使ってExcelのワークシートも更新していきます。

```vb
Do Until rs.EOF
    rs!単価 = Val(rs!単価) + 1
    rs.Update
    rs.MoveNext
Loop
```

#### 3-2-1-2. レコードを追加する

レコードを追加する場合は、次のように記載します。
Recordsetの`AddNewメソッド`でレコードを追加して、`Updateメソッド`でデータの更新を行っています。

```vb:サンプルコード5_2（レコードを追加する）
Sub Sample5_2()
    'インスタンスを作成（事前バインディング）
    Dim cn As New ADODB.Connection
    Dim rs As New ADODB.Recordset
    
    'ADO接続
    cn.Provider = "Microsoft.ACE.OLEDB.12.0"
    cn.Properties("Extended Properties") = "Excel 12.0;HDR=Yes;IMEX=0"
    cn.Open ThisWorkbook.Path & "\" & "TestTable.xlsx"
    
    'SQL文の実行
    rs.Open "SELECT * FROM [Sheet1$A1:E]", cn, , adLockOptimistic
    
    rs.AddNew
    rs!品名 = "ピーマン"
    rs!区分 = "野菜"
    rs!単価 = 120
    rs!購入数 = 3
    rs!購入日 = #11/10/2020#
    rs.Update
    
    'メモリの解放（無くとも構わない）
    rs.Close: Set rs = Nothing
    cn.Close: Set cn = Nothing
End Sub
```

このコードを実行すると、次のように、末尾にレコードが追加されます。
![2020-11-09 231125.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/58c81328-0270-2f9a-babf-9026d4309a52.png)
枠線が無くて格好悪いですが、DBとして扱うなら最初から枠線を入れない方が無難かもしれません。

### 3-2-2. 一括してバッチ更新を行う

これは、もうコードの記載例のみです（申し訳ありません）。
次のようにすることで、バッチ更新をすることが可能です。

```vb:サンプルコード6（バッチ更新で一括処理）
Sub Sample6()
    'インスタンスを作成（事前バインディング）
    Dim cn As New ADODB.Connection
    Dim rs As New ADODB.Recordset
    
    'ADO接続
    cn.Provider = "Microsoft.ACE.OLEDB.12.0"
    cn.Properties("Extended Properties") = "Excel 12.0;HDR=Yes;IMEX=0"
    cn.Open ThisWorkbook.Path & "\" & "TestTable.xlsx"
    
    'SQL文の実行
    rs.CursorLocation = adUseClient
    rs.Open "SELECT * FROM [Sheet1$] WHERE 区分 = '野菜'", cn, adOpenStatic, adLockBatchOptimistic
    Set rs.ActiveConnection = Nothing 'コネクションを一旦切断する
    
    'Recordsetの更新
    Do Until rs.EOF
        rs!単価 = Val(rs!単価) + 2000
        rs.MoveNext
    Loop
    
    rs.ActiveConnection = cn 'コネクションに再接続する
    rs.UpdateBatch 'ここで一括更新する
    
    'メモリの解放（無くとも構わない）
    rs.Close: Set rs = Nothing
    cn.Close: Set cn = Nothing
End Sub
```


# さいごに

ADOは、これまでも色々と使ってきたものの、いざ、記事にまとめようとすると結構分かっていないことがたくさんあり、勉強になりました。

今回の記事作成で参考にさせていただいたWEBサイトは、できるだけリンクを残しましたので、あわせてご覧頂くと理解がしやすいのではないかと思います。