ChangeQuery をもう少し実践的に書いてみる

[ブログからの転載](http://1bed.allright.life/?p=2717)

## 経緯

前回「[SharePoint Online でアイテムの更新状況を取得する(ChangeQuery)](https://1bed.allright.life/?p=2710)」の続き

ChangeQuery を使うことで、SharePoint で行われた変更を取得できることが判ったので、もう少し実践的にコーディングしてみる

ストーリーは、あるサイトのリストとデータベースを同期するという感じで考えてみる

## 環境

### クライアント環境

* Windows 10 Pro
* PowerShell 5.1.17134.165

### SharePoint Online 環境 (同期元)

* Site URL: https://&lt;tenant>.sharepoint.com/sites/example
* リスト名: SyncBase

| 列名 | 内部列名 | 列のタイプ |
| --- | --- | --- |
| タイトル | Title | 1行テキスト |
| コメント | Comment | 1行テキスト |

* 初期データとして、200 件登録済み

  [![](https://1bed.allright.life/wp-content/uploads/2018/07/3-300x230.png)](https://1bed.allright.life/wp-content/uploads/2018/07/3.png)
  こんな感じで、タイトル1 ～ タイトル200 まで

## コード

```powershell
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client")
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.Runtime")

# エラー時に処理を停止する
$ErrorActionPreference = "Stop"

# SharePoint Online の URL
$siteUrl = 'https://<tenant>.sharepoint.com/sites/example'
# ユーザー名
$user = 'miyamiya@example.com';
# パスワード
$secure = Read-Host -Prompt "Enter the password for ${user}(Office365)" -AsSecureString;

try {
	# SharePoint Online 認証情報
	$credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($user, $secure);
	# SharePoint Client Context インスタンスを生成
	$ctx = New-Object Microsoft.SharePoint.Client.ClientContext($siteURL)
	$ctx.Credentials = $credentials
} catch {
    # SharePoint に接続できない時、エラーを出して処理を終了する
    Write-Error ($_.Exception.ToString())
    $ctx.Dispose()
    exit
}
```

ここまではいつものログイン処理

---
```powershell
# 処理開始時間
$procstart = Get-Date

# 前回実行時間ファイル
[string]$ProcDatetimeFile = (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) 'SPSync.datetime')

# 前回の処理開始時間
$ProcessedDatetime = (Get-Date).AddDays(-1) # デフォルト 1 日前
if (Test-Path -Path $ProcDatetimeFile) {
    $ProcessedDatetime = [datetime](Get-Content -Path $ProcDatetimeFile).Trim()
}
```

処理開始時間は、本処理が成功した時に前回実行時間として保存するために取得しています

前回実行時間が保存されているファイルは、スクリプトファイルと同じディレクトリに `SPSync.datetime` として保存されているとし、なければ 1 日前と決め打ちしています

---
```powershell
Write-Host ("前回実行時間: {0}" -f $ProcessedDatetime)

# 対象リスト情報を読み込む
$list = $ctx.Web.Lists.GetByTitle('SyncBase')
$ctx.Load($list)
$ctx.ExecuteQuery()
```
後ほどリストの ID を使うので、先に SyncBase リストの情報を取得しています

---
```powershell
# ChangeQueryの定義
$cq = [Microsoft.SharePoint.Client.ChangeQuery]::new($false, $false)
$cq.Item = $true          # アイテムの更新が対象
$cq.Add = $true           # 追加したアイテム情報が対象
$cq.Update = $true        # 修正したアイテム情報が対象
$cq.DeleteObject = $true  # 削除したアイテム情報が対象
$cq.FetchLimit = 100      # 100 件づつ取得
```

前回と同じで、アイテムの更新(追加・修正・削除)を対象とします

`FetchLimit` を設定して、100 件づつ取得するようにしています  
ちなみに `FetchLimit` は 2000 が上限です

---
```powershell
# ChangeToken オブジェクトの生成
$cq.ChangeTokenStart = [Microsoft.Sharepoint.Client.ChangeToken]::new()

# ChangeTokenStart に前回実行時間をセット
$cq.ChangeTokenStart.StringValue = ("1;3;{0};{1};-1" -f $list.Id.ToString(), ($ProcessedDatetime.ToUniversalTime().Ticks.ToString()))
```

範囲を指定して変更ログを読み取るためには、ChangeToken オブジェクトを使うため生成しています

`ChangeTokenStart` に前回実行時間をセットする、`ChangeToken.StringValue` を書式に沿って生成しています

`ChangeToken.StringValue` の書式は次の通り

> 例： 1;3;xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;636678394680000000;-1

1. バージョン番号
2. スコープ(0: Content Database、1: Site Collection、2: Site、3: List)
3. スコープで指定したリソースの GUID
4. 時間
5. ChangeToken の変更アイテム値(-1 で初期化)

---
```powershell
try {
    
    # 永久ループ、リミッターを付けるために for 文でも良い
    while(1)
    {
        # 対象リストから更新のあったアイテムを取得
        $changeItems = $list.GetChanges($cq)
        $ctx.Load($changeItems)
        $ctx.ExecuteQuery()

        Write-Host ("Change Item Count: {0}" -f $changeItems.Count.ToString())

        # 前回実行時から変更されたアイテムがない
        if ($changeItems.Count -eq 0) {
            break
        }

        foreach($item in $changeItems){
            switch ($item.ChangeType) {
                "Add" {
                    Write-Host ("ChangeType Add.  ItemId : {0}" -f $item.ItemId)
                }
                "Update" {
                    Write-Host ("ChangeType Update. ItemId : {0}" -f $item.ItemId)
                }
                "DeleteObject" {
                    Write-Host ("ChangeType Delete. ItemId : {0}" -f $item.ItemId)
                }
            }
            # 最後のトークンを取得する
            $lastToken = ([Microsoft.SharePoint.Client.Change]$item).ChangeToken.StringValue
        }
        # 取得した最後のアイテムの ChangeToken.StringValue をセット
        $cq.ChangeTokenStart.StringValue = $lastToken
    }

    # 処理が正常終了したら処理開始時間をファイルに書き込む
    $procstart.ToString('yyyy/MM/dd HH:mm:ss') | Out-File -FilePath $ProcDatetimeFile

} catch {
    # 例外が出たらエラーを出す
    Write-Error ($_.Exception.ToString())

} finally {
    # Context を破棄
    $ctx.Dispose()

}
```

ポイントは、 **「最後のトークンを取得する」** の次の行で、処理したアイテムの `ChangeToken` を取得して、再度 `$list.GetChanges($cq)` する前に、`ChangeTokenStart.StringValue` へ `$lastToken` をセットしていること

こうすることで、1 度に 2000 件(FetchLimit の上限)までしか取得できない ChangeQuery を繰り返して取得することができる

## 実行結果

### リスト SyncBase にアイテムを 200 件追加して実行

```powershell
前回実行時間: 2018/07/21 21:53:19
Change Item Count: 100
ChangeType Add.  ItemId : 1
ChangeType Add.  ItemId : 2
　：
(省略)
　：
ChangeType Add.  ItemId : 99
ChangeType Add.  ItemId : 100
Change Item Count: 100
ChangeType Add.  ItemId : 101
ChangeType Add.  ItemId : 102
　：
(省略)
　：
ChangeType Add.  ItemId : 199
ChangeType Add.  ItemId : 200
Change Item Count: 0
```

100 件づつのページングを経て、200 件の変更ログが取得できている

### リスト SyncBase の 1 件目 と 200 件目のみ更新して実行

```powershell
前回実行時間: 2018/07/22 21:53:19
Change Item Count: 2
ChangeType Update. ItemId : 1
ChangeType Update. ItemId : 200
Change Item Count: 0
```

これにより、アイテム 100 件づつページングが必要ではなく、更新ログ 100 件づつでページングが必要ということが判る


## 考察

少しまともに書こうと思ったら長くなってしまった... \_(:3｣∠)_

サンプルコード一式は [Github](https://github.com/miyamiya/Learnning_PowerShell/blob/master/ChangeQueryExample.ps1) に置いてます